Monitorando arquivos e pastas no Linux com systemd path units (e inotifywait para quem não tem root)

Neste post
No post anterior, o launchd do macOS monitorava arquivos e diretórios com WatchPaths para disparar scripts automaticamente quando algo mudava. O modelo é reativo — em vez de rodar um backup a cada hora ou uma conversão a cada cinco minutos, o sistema observa o caminho no disco e só executa o job quando detecta uma modificação real. Sem polling, sem desperdício, sem janela de vulnerabilidade entre a mudança e a ação.
O Linux tem a mesma capacidade, mas implementada de forma diferente e com mais opções. O systemd oferece path units — arquivos .path que monitoram caminhos no sistema de arquivos e ativam automaticamente um service associado quando a condição é satisfeita. É o equivalente direto do WatchPaths do launchd, com a mesma filosofia declarativa: você descreve o que vigiar num arquivo de configuração, o sistema cuida do resto. Para quem trabalha em servidores ou desktops com systemd, que a essa altura é praticamente qualquer distribuição mainstream, os path units são a ferramenta certa.
Mas nem todo mundo tem systemd à disposição. Containers mínimos, distribuições como Alpine ou Void Linux, ambientes compartilhados onde o usuário não tem controle sobre os serviços do sistema, máquinas legadas — existem cenários legítimos onde o systemd não está disponível ou onde o usuário simplesmente não pode criar units. Para esses casos, o inotifywait do pacote inotify-tools resolve o problema diretamente no shell, usando a mesma infraestrutura de notificação do kernel (inotify) que o systemd usa por baixo, mas sem precisar de daemon, de root, nem de arquivo de configuração.
Este post aplica os mesmos dois cenários do post anterior — backup reativo de um banco SQLite e otimização automática de imagens — em ambiente Linux, primeiro com systemd path units e depois com inotifywait. O script de conversão de imagens já está publicado; o foco aqui é na mecânica dos gatilhos e na construção dos arquivos de configuração.
systemd path units: a versão Linux do WatchPaths#
Quem leu o post sobre systemd timers já conhece o padrão: o systemd divide responsabilidades entre units de tipos diferentes, e cada tipo cuida de um aspecto do job. Um timer (.timer) define quando um service (.service) deve rodar. Um path (.path) faz a mesma coisa, mas o gatilho não é o relógio — é o sistema de arquivos. O .path observa, o .service executa. São dois arquivos que trabalham juntos, da mesma forma que o timer e o service trabalham juntos para agendamento por horário.
A separação pode parecer burocrática comparada com o launchd, onde um único plist contém tanto o gatilho (WatchPaths) quanto o comando a executar (ProgramArguments). Na prática, a divisão traz a mesma vantagem que já apareceu nos timers: o service pode ser testado independentemente do path. Você roda systemctl start meu-backup.service para verificar que o script funciona, e só depois ativa o meu-backup.path para colocar o monitoramento em produção. Se o service falhar, o problema está no script, não no gatilho. Se o path não disparar, o problema está no monitoramento, não no script. Cada peça pode ser diagnosticada isoladamente, o que em sistemas que rodam sem supervisão direta — servidores headless, VPSs, containers — faz diferença real quando algo quebra às três da manhã.
A detecção de mudanças no Linux usa o subsistema inotify do kernel, que é o equivalente do kqueue no macOS. O inotify existe desde o kernel 2.6.13 (2005, coincidentemente o mesmo ano em que o launchd apareceu no macOS) e é a infraestrutura padrão para notificação de eventos do sistema de arquivos no Linux. Ferramentas como tail -f, o hot-reload do Webpack, o watchdog do Python e o próprio systemd usam inotify por baixo. Não há polling envolvido — o kernel notifica o processo observador quando o evento acontece, com latência negligível e consumo de recursos próximo de zero enquanto nada muda.
Como funciona um .path + .service#
PathChanged, PathModified e PathExists#
O launchd tem uma única chave — WatchPaths — que dispara em qualquer modificação. O systemd é mais granular e oferece três diretivas diferentes para monitoramento de caminhos, cada uma com semântica própria.
PathModified dispara quando o conteúdo do arquivo é alterado — ou seja, quando uma escrita efetivamente muda dados no arquivo. É o mais próximo do WatchPaths do launchd para monitoramento de arquivos individuais. Se o que importa é saber que o banco SQLite recebeu novas transações ou que um arquivo de configuração foi editado, PathModified é a diretiva certa.
PathChanged dispara quando o arquivo é fechado após ter sido modificado. A diferença em relação ao PathModified é temporal: enquanto PathModified pode disparar durante a escrita (a cada flush do buffer, por exemplo), PathChanged espera o arquivo ser fechado para disparar. Para scripts de backup, essa diferença importa — disparar o backup enquanto o arquivo ainda está sendo escrito pode resultar numa cópia inconsistente. Na prática, PathChanged é a escolha mais segura para a maioria dos casos porque garante que a escrita terminou antes de ativar o service.
PathExists dispara quando o caminho especificado passa a existir. Não monitora modificações — apenas a criação. Se o arquivo já existir quando o path unit for ativado, o service dispara imediatamente. É útil para cenários de tipo semáforo: um processo cria um arquivo sinalizador quando termina seu trabalho, e o path unit detecta a presença desse arquivo para iniciar a próxima etapa. Para os cenários deste post, PathExists não é a ferramenta certa.
Existe ainda o PathExistsGlob, que funciona como PathExists mas aceita padrões glob — *.csv, backup-*.sql, etc. Dispara quando qualquer arquivo que corresponda ao padrão passa a existir no diretório. Parece tentador para o cenário de otimização de imagens (monitorar *.png e *.jpg), mas na prática tem a mesma limitação do PathExists: só detecta criação, não modificação subsequente. Se o arquivo for criado vazio e preenchido depois — como alguns editores fazem — o service pode disparar antes de o conteúdo estar pronto.
As três diretivas podem monitorar tanto arquivos quanto diretórios. Quando o caminho é um diretório, PathModified e PathChanged disparam quando a estrutura do diretório muda — criação, remoção ou renomeação de arquivos dentro dele. O comportamento é análogo ao do WatchPaths do launchd com diretórios, incluindo a mesma limitação: o monitoramento não é recursivo. Subpastas não são observadas automaticamente; cada caminho relevante precisa de sua própria diretiva.
Múltiplas diretivas podem coexistir no mesmo .path. Um único path unit pode ter um PathChanged para um arquivo e um PathModified para outro, e o service associado dispara quando qualquer uma das condições for satisfeita. Essa flexibilidade não existe no launchd, onde cada plist tem um único WatchPaths — embora o array do WatchPaths aceite múltiplos caminhos, o tipo de evento monitorado é sempre o mesmo para todos eles.
A relação entre o .path e o .service#
O vínculo entre o .path e o .service é por convenção de nome: um path unit chamado meu-backup.path ativa automaticamente o service meu-backup.service, sem precisar de nenhuma diretiva explícita para conectar os dois. Se por alguma razão o service tiver um nome diferente, a diretiva Unit= na seção [Path] permite especificar qual service ativar, mas na prática manter o mesmo nome é mais simples e mais legível.
Quando o path unit detecta uma mudança, ele ativa o service associado — que roda, executa o script, e termina. O path unit continua observando. Na próxima mudança, o service é ativado de novo. O ciclo se repete indefinidamente enquanto o path unit estiver habilitado. É exatamente o mesmo modelo do launchd: o gatilho observa, o script executa e termina, o gatilho continua observando.
Um detalhe que difere do launchd: se o service ainda estiver rodando quando uma nova mudança for detectada, o systemd não enfileira uma segunda execução. A mudança é registrada, mas o service não é ativado novamente até que a execução atual termine. Na prática, isso significa que se o script de backup demorar dois minutos e o banco for modificado três vezes durante esse período, o service roda uma vez quando terminar a execução atual — não três vezes. Para scripts que varrem o diretório inteiro a cada execução (como o otimizador de imagens), esse comportamento é desejável. Para scripts que processam um único evento por execução, significa que modificações intermediárias podem ser “agrupadas” numa única ativação.
A forma mínima dos dois arquivos juntos é a seguinte:
~/.config/systemd/user/meu-backup.path:
[Path]
PathChanged=/home/janio/dados/arquivo.db
[Install]
WantedBy=default.target
~/.config/systemd/user/meu-backup.service:
[Service]
ExecStart=/home/janio/bin/meu-script.sh
Dois arquivos, quatro linhas de configuração no total (sem contar a seção [Install]). O .path diz o que vigiar; o .service diz o que rodar. A ativação é feita com dois comandos:
systemctl --user enable --now meu-backup.path
O --user é o detalhe que faz tudo funcionar sem root, e merece uma seção própria.
User units: sem root, sem problema#
Todas as units mostradas neste post vivem em ~/.config/systemd/user/ e são gerenciadas com systemctl --user. Não precisam de root para criar, não precisam de root para ativar, e rodam com as permissões do usuário logado. É o equivalente direto dos LaunchAgents em ~/Library/LaunchAgents/ no macOS — automação pessoal, sem escalar privilégios, sem tocar na configuração do sistema.
As user units têm, por padrão, uma limitação importante: elas só rodam enquanto o usuário tem uma sessão ativa. Se o usuário fizer logout, as units param. Em desktops com login gráfico permanente isso raramente é um problema, mas em servidores acessados por SSH faz diferença. A solução é o loginctl enable-linger, que permite que as units do usuário continuem rodando mesmo sem sessão ativa. No macOS, esse problema não existe — LaunchAgents pessoais permanecem ativos enquanto o usuário estiver logado na sessão gráfica, e o conceito de “logout” num Mac pessoal é raro o suficiente para não ser uma preocupação prática.
Para os cenários deste post, user units são a escolha certa. O banco SQLite é um arquivo do usuário, as imagens são do usuário, os scripts rodam no contexto do usuário, e o backup vai para um remote configurado no rclone do usuário. Nada aqui precisa de acesso privilegiado, e rodar como root seria um exagero desnecessário — além de um risco, já que qualquer bug no script teria permissão para danificar o sistema inteiro em vez de apenas os arquivos do usuário.
Cenário 1: backup reativo de um banco SQLite#
O .path#
~/.config/systemd/user/fuqu-backup.path:
[Path]
PathChanged=/home/janio/.local/share/fuqu/fuqu.db
[Install]
WantedBy=default.target
Uma diretiva, um caminho. O PathChanged em vez de PathModified é deliberado: como o SQLite no modo WAL faz múltiplas escritas em sequência (o WAL cresce, o checkpoint consolida), o PathModified poderia disparar o service várias vezes durante uma única operação de escrita. O PathChanged espera o arquivo ser fechado, o que na prática significa que o service só é ativado depois que a transação terminou e o banco está num estado consistente.
No post anterior sobre launchd, o WatchPaths apontava para o arquivo fuqu.db em vez do diretório, porque monitorar o diretório poderia não capturar escritas feitas diretamente no arquivo pelo SQLite. O mesmo raciocínio se aplica aqui: o PathChanged aponta para o banco em si, não para ~/.local/share/fuqu/.
O .service#
~/.config/systemd/user/fuqu-backup.service:
[Service]
Type=oneshot
ExecStart=/home/janio/bin/fuqu-backup.sh
Environment=PATH=/usr/local/bin:/usr/bin:/bin
O Type=oneshot diz ao systemd que o service executa uma tarefa e termina — não é um daemon que fica rodando. Sem essa diretiva, o systemd assume Type=simple e espera que o processo permaneça ativo; quando o script terminar, o systemd interpretaria a saída como uma falha. O oneshot é o tipo correto para scripts de backup, conversões, sincronizações e qualquer job que roda, faz seu trabalho e sai.
O Environment=PATH=... resolve o mesmo problema que o EnvironmentVariables no plist do launchd: o systemd não herda o PATH do shell do usuário. Se o rclone foi instalado em /usr/local/bin ou via snap em /snap/bin, o caminho precisa estar explícito. Uma alternativa é usar o caminho absoluto do rclone diretamente no script, mas definir o PATH no service mantém o script portável entre máquinas onde o binário pode estar em locais diferentes.
O logging, diferente do launchd onde precisávamos declarar StandardOutPath e StandardErrorPath no plist, vem de graça. O systemd captura stdout e stderr automaticamente e direciona para o journal. Para ver a saída do último backup:
journalctl --user -u fuqu-backup.service -n 50
Para acompanhar em tempo real enquanto testa:
journalctl --user -u fuqu-backup.service -f
Sem arquivo de log para rotacionar, sem disco enchendo silenciosamente com meses de saída acumulada. O journal cuida da retenção e do espaço automaticamente — uma das vantagens concretas do systemd sobre o modelo de log manual do launchd.
A ativação dos dois arquivos é um único comando:
systemctl --user enable --now fuqu-backup.path
O enable registra o path unit para iniciar automaticamente no login. O --now ativa imediatamente sem esperar o próximo login. A partir desse momento, qualquer modificação no fuqu.db dispara o fuqu-backup.service. Para verificar que o monitoramento está ativo:
systemctl --user status fuqu-backup.path
Throttle: por que o script ainda precisa se proteger#
O systemd não tem um throttle automático equivalente ao intervalo de 10 segundos do launchd. Se o banco for modificado, o service roda. Se o banco for modificado de novo um segundo depois e o service já tiver terminado, ele roda de novo. Não há intervalo mínimo forçado entre execuções.
Isso torna o throttle no script ainda mais necessário do que no macOS. O mecanismo é o mesmo descrito no post anterior: o script grava um timestamp num arquivo de controle após cada backup, e na próxima execução verifica se o intervalo mínimo já passou antes de fazer qualquer trabalho. Se não passou, encerra com código 0 e o systemd registra a execução como bem-sucedida.
O systemd tem uma diretiva RateLimitIntervalSec combinada com RateLimitBurst na seção [Unit] que poderia, em tese, limitar a frequência de ativação. Mas essas diretivas controlam o rate limit do próprio systemd para ativações do service — quando o limite é atingido, o systemd desabilita o path unit temporariamente, o que significa que modificações durante esse período são silenciosamente ignoradas. Não é um throttle com garantia de execução eventual; é um circuit breaker que descarta eventos. Para um backup onde nenhuma modificação deve ser perdida, o throttle no script é a abordagem correta: a execução acontece, verifica que é cedo demais, e sai limpa — mas o path unit continua ativo e pronto para disparar na próxima mudança.
Cenário 2: otimização automática de imagens#
O .path#
~/.config/systemd/user/image-optimizer.path:
[Path]
PathChanged=/home/janio/Pictures/optimize
[Install]
WantedBy=default.target
Aqui o PathChanged aponta para um diretório, não para um arquivo. O comportamento é o esperado: o systemd dispara o service quando a estrutura do diretório muda — um arquivo criado, removido ou renomeado. Salvar um PNG em ~/Pictures/optimize/ é uma criação de arquivo, que altera o diretório, que dispara o path unit.
A tentação de usar PathExistsGlob com padrões como *.png e *.jpg aparece naturalmente aqui, mas não resolve o problema. O PathExistsGlob dispara quando um arquivo correspondente ao padrão passa a existir, e dispara uma única vez — depois que o service roda, o path unit não reage a novos arquivos que correspondam ao mesmo glob até ser reiniciado. É uma diretiva pensada para cenários de semáforo (“espere até que este arquivo apareça”), não para monitoramento contínuo. O PathChanged no diretório é a escolha correta para um fluxo onde imagens podem chegar a qualquer momento e em qualquer quantidade.
O .service#
~/.config/systemd/user/image-optimizer.service:
[Service]
Type=oneshot
ExecStart=/home/janio/bin/optimize-images.sh
Environment=PATH=/usr/local/bin:/usr/bin:/bin
A estrutura é idêntica ao service de backup. O Type=oneshot porque o script processa as imagens e termina. O PATH inclui /usr/local/bin onde o cwebp e o avifenc podem estar — no Linux, diferente do macOS com Homebrew em /opt/homebrew/bin, esses pacotes tipicamente vêm do gerenciador de pacotes da distribuição e instalam em /usr/bin ou /usr/local/bin. Em distribuições baseadas em Debian, os pacotes são webp e libavif-bin:
sudo apt install webp libavif-bin
O logging segue o mesmo modelo: stdout e stderr vão para o journal automaticamente. Para verificar o resultado de uma conversão:
journalctl --user -u image-optimizer.service -n 20
Ativação:
systemctl --user enable --now image-optimizer.path
A partir desse momento, qualquer PNG ou JPG salvo em ~/Pictures/optimize/ dispara o script de conversão.
O mesmo cuidado com loops#
O problema de loops descrito no post sobre launchd se aplica igualmente aqui, com a mesma causa e a mesma solução. Se o script converte foto.png para foto.avif dentro do mesmo diretório monitorado, a criação do .avif é uma modificação no diretório, que dispara o path unit, que executa o script de novo. O script precisa filtrar por extensão — processar apenas .png, .jpg e .jpeg, ignorar todo o resto — ou a separação em dois diretórios (entrada e saída) elimina o problema na raiz.
No systemd, existe um agravante que o launchd não tem: como não há throttle automático de 10 segundos entre execuções, um loop causado por falta de filtro pode ser mais agressivo. O script cria o .avif, o path unit dispara imediatamente, o script roda de novo, não encontra nada para processar (se o filtro estiver correto) e sai. Mas se o filtro não estiver correto e o script tentar reprocessar o .avif, a sequência se repete sem nenhum freio externo até que o RateLimitBurst padrão do systemd intervenha e desabilite o path unit — o que resolve o loop mas também mata o monitoramento legítimo.
A proteção correta é dupla: o filtro por extensão no script garante que a execução termine sem efeitos colaterais quando não há trabalho real a fazer, e um exit 0 rápido no início do script quando o find não retorna arquivos elegíveis evita que o systemd sequer registre atividade significativa no journal. O path unit dispara, o script olha o diretório, não encontra PNGs nem JPGs, e encerra em milissegundos. O custo de uma execução vazia é negligível; o custo de um loop sem proteção pode ser um path unit desabilitado e imagens que param de ser convertidas sem aviso.
Para quem não tem systemd: inotifywait#
O que é e de onde vem#
O systemd path units e o WatchPaths do launchd são abstrações declarativas sobre mecanismos do kernel — inotify no Linux, kqueue no macOS. Eles escondem a complexidade atrás de arquivos de configuração e gerenciam o ciclo de vida do processo automaticamente. Mas a abstração exige o sistema de init correspondente. Em máquinas sem systemd — containers Docker mínimos, Alpine Linux, distribuições que usam OpenRC ou runit, servidores compartilhados onde o usuário não controla os serviços — os path units não existem.
O inotifywait é a forma de acessar o inotify do kernel diretamente a partir do shell, sem intermediários. Faz parte do pacote inotify-tools, disponível nos repositórios de praticamente toda distribuição Linux. Em Debian e Ubuntu:
sudo apt install inotify-tools
Em Alpine:
apk add inotify-tools
O inotifywait bloqueia até que um evento ocorra no caminho monitorado e então imprime o evento e sai — ou, com a flag -m (monitor), continua rodando e imprimindo eventos indefinidamente. Não é um daemon, não precisa de arquivo de configuração, não precisa de root. É um comando que observa e reporta, e a lógica de reação fica por conta de quem consome sua saída — tipicamente um while read num script bash.
A granularidade dos eventos é maior do que qualquer coisa disponível no launchd ou nos systemd path units. O inotify distingue entre create, modify, close_write, delete, moved_to, moved_from, attrib e mais uma dezena de tipos de evento. O inotifywait permite filtrar por qualquer combinação deles com a flag -e. Onde o PathChanged do systemd agrupa vários eventos numa semântica de “o arquivo foi fechado após modificação”, o inotifywait permite escolher exatamente quais eventos importam — e ignorar o resto.
Cenário 1 com inotifywait#
Para o backup reativo do banco SQLite, o inotifywait monitora o arquivo fuqu.db e dispara o script de backup quando o arquivo é modificado e fechado:
inotifywait -m -e close_write /home/janio/.local/share/fuqu/fuqu.db |
while read dir event file; do
/home/janio/bin/fuqu-backup.sh
done
O evento close_write é a escolha certa aqui — dispara quando o arquivo é fechado após ter sido aberto para escrita. É o equivalente funcional do PathChanged do systemd: garante que a transação do SQLite terminou antes de iniciar o backup. Usar modify em vez de close_write dispararia o script a cada flush do buffer, potencialmente múltiplas vezes durante uma única operação de escrita.
O comando bloqueia no terminal. Para rodar em background de forma persistente, as opções são colocá-lo dentro de um script e executar com nohup, dentro de uma sessão tmux ou screen, ou — ironicamente — dentro de um service do systemd ou de um supervisor como o runit ou o s6. Sem um supervisor, se o processo morrer (OOM, kill acidental, crash do terminal), o monitoramento para e ninguém avisa. É a desvantagem fundamental do inotifywait comparado com o launchd e o systemd: ele faz o monitoramento, mas não cuida de si mesmo.
O throttle funciona da mesma forma que nos cenários anteriores — o script de backup verifica o timestamp do último backup antes de executar. A diferença é que com inotifywait o throttle é ainda mais importante, porque não há nenhum mecanismo externo limitando a frequência de chamadas. Cada close_write no banco dispara uma invocação do script, e em uso ativo do FUQU isso pode significar dezenas de invocações por minuto. Sem o throttle no script, cada uma delas tentaria conectar ao Backblaze B2.
Cenário 2 com inotifywait#
Para a otimização de imagens, o inotifywait monitora o diretório e reage à criação de novos arquivos:
inotifywait -m -e close_write --include '\.(png|jpg|jpeg)$' \
/home/janio/Pictures/optimize/ |
while read dir event file; do
/home/janio/bin/optimize-images.sh
done
O --include com uma expressão regular filtra os eventos antes de chegarem ao while read. Apenas arquivos com extensão .png, .jpg ou .jpeg disparam o script. Um .avif criado pelo próprio script de conversão gera um evento close_write no diretório, mas o inotifywait o ignora porque não corresponde ao filtro. O problema de loop que precisava ser tratado no script tanto no launchd quanto no systemd é resolvido aqui no próprio comando de monitoramento — uma camada antes.
Essa é uma vantagem concreta do inotifywait sobre as alternativas declarativas: o filtro por padrão de nome acontece na ferramenta de monitoramento, não no script que processa os arquivos. O WatchPaths do launchd não aceita filtros — dispara para qualquer mudança, e o script decide o que fazer. O PathChanged do systemd também não filtra por nome de arquivo. O inotifywait permite ser seletivo antes de o script sequer ser chamado, o que reduz execuções desnecessárias e simplifica a lógica do script.
O evento close_write em vez de create resolve o problema de arquivos parcialmente escritos que foi discutido no post anterior. O create dispara no instante em que o arquivo aparece no diretório, antes de o conteúdo ser gravado por completo. Um PNG de 10 MB sendo salvo por um navegador dispara create quando o download começa e close_write quando termina. Monitorar close_write garante que o arquivo está completo antes de o script tentar convertê-lo.
Limitações e quando vale a pena#
O inotifywait é poderoso como mecanismo de monitoramento, mas frágil como infraestrutura de automação. As limitações são todas relacionadas à ausência de um supervisor:
O processo precisa estar rodando para monitorar. Se ele morre, o monitoramento para. Não há equivalente do enable do systemd que garante reinício automático no boot ou após um crash. Cabe ao usuário garantir que o inotifywait esteja rodando — via nohup, tmux, crontab com @reboot, ou qualquer outro mecanismo externo de persistência.
Não há recuperação de eventos perdidos. Se o inotifywait não estava rodando quando o banco SQLite foi modificado, a modificação passa despercebida. O launchd com WatchPaths verifica o estado dos caminhos monitorados quando o agent é carregado e dispara se algo mudou durante o período de inatividade. O systemd faz o mesmo quando o path unit é ativado. O inotifywait não tem essa memória — ele observa a partir do momento em que começa a rodar, e tudo que aconteceu antes não existe.
Não há logging integrado. A saída vai para stdout, e se ninguém a capturou, desapareceu. Redirecionar para um arquivo de log é trivial (>> /var/log/meu-monitor.log 2>&1), mas a rotação e a retenção ficam por conta do usuário.
Dito isso, o inotifywait vale a pena em contextos específicos: ambientes sem systemd onde instalar um init system alternativo seria desproporcional ao problema, scripts descartáveis que precisam monitorar um diretório por algumas horas durante uma migração ou importação, e situações onde a granularidade dos eventos justifica a complexidade extra — filtrar por tipo de evento e padrão de nome diretamente no comando de monitoramento é algo que nem o launchd nem o systemd oferecem com a mesma precisão. Para automação permanente numa máquina com systemd, os path units são a escolha certa. Para um monitoramento pontual ou num ambiente restrito, o inotifywait faz o trabalho com zero dependências além do kernel.
launchd, systemd e inotifywait: comparação rápida#
Três posts, três ferramentas, o mesmo objetivo. A tabela abaixo resume as diferenças práticas para quem precisa escolher — ou para quem usa mais de um sistema e quer saber o que muda.
| launchd (WatchPaths) | systemd (path units) | inotifywait | |
|---|---|---|---|
| Sistema | macOS | Linux com systemd | Qualquer Linux |
| Configuração | plist XML | .path + .service (INI) | Comando no shell |
| Precisa de root | Não (LaunchAgent) | Não (user unit) | Não |
| Mecanismo do kernel | kqueue | inotify | inotify |
| Filtro por tipo de evento | Não | Parcial (PathChanged vs PathModified) | Sim (qualquer evento inotify) |
| Filtro por nome de arquivo | Não | Não (exceto PathExistsGlob) | Sim (–include / –exclude) |
| Monitoramento recursivo | Não | Não | Sim (-r) |
| Throttle automático | Sim (10s, ajustável para cima) | Não (RateLimitBurst desabilita o path) | Não |
| Recuperação de eventos perdidos | Sim (verifica estado ao carregar) | Sim (verifica estado ao ativar) | Não |
| Logging | Manual (StandardOutPath) | Automático (journal) | Manual (redirecionamento de stdout) |
| Supervisão do processo | Automática (launchd reinicia) | Automática (systemd reinicia) | Nenhuma (precisa de supervisor externo) |
| Teste independente | launchctl start | systemctl start .service | Executar o script diretamente |
A coluna do inotifywait é a que mais se destaca nos extremos: maior granularidade de eventos e filtros, mas nenhuma supervisão nem recuperação. É a ferramenta mais poderosa como mecanismo de observação e a mais frágil como infraestrutura de automação. O launchd e o systemd convergem na maioria dos aspectos práticos, com diferenças pontuais — o throttle automático do launchd, o logging integrado do systemd, a granularidade de eventos do systemd — que refletem as filosofias diferentes dos dois sistemas.
Para os dois cenários deste post e do anterior — backup reativo e otimização de imagens — qualquer uma das três ferramentas resolve o problema. A escolha é ditada pelo sistema operacional e pelo nível de acesso disponível, não pela capacidade técnica da ferramenta. No Mac, launchd. No Linux com systemd, path units. No Linux sem systemd ou sem controle sobre os serviços, inotifywait.
O que fica para os próximos posts#
Este post e o anterior cobriram o mesmo território — monitoramento reativo de arquivos e diretórios — nos dois sistemas operacionais que um sysadmin provavelmente usa no dia a dia. Os plists do launchd, os path units do systemd e o inotifywait são três formas de dizer a mesma coisa: “quando isso mudar, rode aquilo”. A mecânica dos gatilhos está resolvida.
O que falta são os scripts que os gatilhos disparam. O de backup do SQLite precisa lidar com WAL checkpoint, staging directory, rclone configurado com um remote no Backblaze B2, e o mecanismo de throttle que apareceu como conceito nos dois posts mas ainda não virou código. O script de conversão de imagens já está pronto — com fallback entre encoders, proteção contra arquivos incompletos, e configuração de formato de saída e destino dos originais.
São problemas diferentes do monitoramento — menos sobre configuração de sistema e mais sobre lógica de aplicação — e merecem espaço próprio para serem tratados com o detalhe que precisam. Os próximos posts vão entregar o código completo de cada um.
Sysadmin, 53 anos, brasileiro trabalhando de casa para o mundo todo. Cuida de servidores Linux, containers LXC, e de gatos que não saem de cima do teclado.