From 655dcb81e9e9fabb295b16a100e4b2828aac8a9b Mon Sep 17 00:00:00 2001 From: reugenio Date: Sun, 7 Dec 2025 23:45:07 +0100 Subject: [PATCH] =?UTF-8?q?Daemon=20mode=20+=20configuraci=C3=B3n=20extern?= =?UTF-8?q?a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nuevo m贸dulo daemon.zig: fork() + setsid() + /dev/null - Opci贸n --daemon/-d para ejecutar en background - Archivo PID en service-monitor.pid - Config externo desde archivo (--config/-c) - Formato CSV simple: http,nombre,url / tcp,nombre,host,puerto - Soporte para email y telegram en config (preparado) - services.conf.example con documentaci贸n - Ayuda actualizada con todas las opciones 馃 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- services.conf.example | 46 ++++++++ src/config.zig | 249 ++++++++++++++++++++++++++++++++++-------- src/daemon.zig | 100 +++++++++++++++++ src/main.zig | 202 +++++++++++++++++++++------------- 4 files changed, 473 insertions(+), 124 deletions(-) create mode 100644 services.conf.example create mode 100644 src/daemon.zig diff --git a/services.conf.example b/services.conf.example new file mode 100644 index 0000000..2323f8b --- /dev/null +++ b/services.conf.example @@ -0,0 +1,46 @@ +# Service Monitor - Archivo de configuraci贸n +# ============================================ +# +# Formato: tipo,par谩metros separados por comas +# Las l铆neas que empiezan con # son comentarios +# +# SERVICIOS +# --------- +# http,Nombre,URL +# tcp,Nombre,host,puerto + +# Servicios HTTP/HTTPS +http,Forgejo (HTTP),https://git.reugenio.com +http,Simifactu API,https://simifactu.com +http,Mundisofa,https://mundisofa.com +http,Menzuri,https://menzuri.com + +# Servicios TCP +tcp,Forgejo (SSH),git.reugenio.com,2222 + +# NOTIFICACIONES EMAIL +# -------------------- +# email,destinatario@ejemplo.com +# email_smtp,servidor,puerto,usuario,password,remitente + +# Destinatarios (uno por l铆nea) +#email,admin@ejemplo.com +#email,alertas@ejemplo.com + +# Servidor SMTP +#email_smtp,smtp.gmail.com,587,usuario@gmail.com,app_password,alertas@ejemplo.com + +# NOTIFICACIONES TELEGRAM +# ----------------------- +# telegram,bot_token,chat_id +# +# Para obtener el bot_token: +# 1. Habla con @BotFather en Telegram +# 2. Usa /newbot y sigue las instrucciones +# 3. Copia el token que te da +# +# Para obtener tu chat_id: +# 1. Habla con @userinfobot en Telegram +# 2. Te dir谩 tu chat_id + +#telegram,123456789:ABCdefGHIjklMNOpqrsTUVwxyz,987654321 diff --git a/src/config.zig b/src/config.zig index e84ca0a..51f2416 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1,7 +1,21 @@ //! Configuraci贸n de servicios a monitorear //! -//! Define la lista de servicios del servidor Simba (Hetzner) que deben -//! ser verificados peri贸dicamente. +//! Soporta configuraci贸n desde archivo externo o valores por defecto. +//! Formato del archivo de configuraci贸n: +//! +//! ``` +//! # Comentarios empiezan con # +//! # Servicios HTTP/HTTPS +//! http,Nombre Servicio,https://ejemplo.com +//! +//! # Servicios TCP +//! tcp,Nombre Servicio,host.ejemplo.com,puerto +//! +//! # Configuraci贸n de notificaciones +//! email,destinatario@ejemplo.com +//! email_smtp,smtp.ejemplo.com,587,usuario,password +//! telegram,bot_token,chat_id +//! ``` const std = @import("std"); @@ -20,60 +34,199 @@ pub const Service = struct { /// Tipo de verificaci贸n a realizar. check_type: CheckType, /// URL completa para verificaciones HTTP (ej: "https://example.com"). - /// Solo usado cuando check_type == .http url: []const u8 = "", /// Hostname para verificaciones TCP. - /// Solo usado cuando check_type == .tcp host: []const u8 = "", /// Puerto para verificaciones TCP. - /// Solo usado cuando check_type == .tcp port: u16 = 0, }; -/// Lista de servicios del servidor Simba (188.245.244.244) a monitorear. -/// -/// Servicios actuales: -/// - Forgejo (Git): HTTP en git.reugenio.com + SSH en puerto 2222 -/// - Simifactu API: HTTPS en simifactu.com -/// - Mundisofa: HTTPS en mundisofa.com -/// - Menzuri: HTTPS en menzuri.com -const services = [_]Service{ - // Forgejo - Servidor Git - .{ - .name = "Forgejo (HTTP)", - .check_type = .http, - .url = "https://git.reugenio.com", - }, - .{ - .name = "Forgejo (SSH)", - .check_type = .tcp, - .host = "git.reugenio.com", - .port = 2222, - }, - // Simifactu - API facturaci贸n - .{ - .name = "Simifactu API", - .check_type = .http, - .url = "https://simifactu.com", - }, - // Mundisofa - Tienda online - .{ - .name = "Mundisofa", - .check_type = .http, - .url = "https://mundisofa.com", - }, - // Menzuri - Web - .{ - .name = "Menzuri", - .check_type = .http, - .url = "https://menzuri.com", - }, +/// Configuraci贸n SMTP para env铆o de emails. +pub const SmtpConfig = struct { + host: []const u8 = "", + port: u16 = 587, + username: []const u8 = "", + password: []const u8 = "", + from: []const u8 = "", }; -/// Retorna la lista de servicios a monitorear. +/// Configuraci贸n de Telegram. +pub const TelegramConfig = struct { + bot_token: []const u8 = "", + chat_id: []const u8 = "", +}; + +/// Configuraci贸n completa cargada desde archivo. +pub const Config = struct { + /// Lista de servicios a monitorear. + services: []Service, + /// Emails destinatarios para alertas. + email_recipients: [][]const u8, + /// Configuraci贸n SMTP. + smtp: SmtpConfig, + /// Configuraci贸n Telegram. + telegram: TelegramConfig, + /// Allocator usado (para liberar memoria). + allocator: std.mem.Allocator, + + /// Libera toda la memoria de la configuraci贸n. + pub fn deinit(self: *Config) void { + for (self.services) |service| { + self.allocator.free(service.name); + if (service.url.len > 0) self.allocator.free(service.url); + if (service.host.len > 0) self.allocator.free(service.host); + } + self.allocator.free(self.services); + + for (self.email_recipients) |email| { + self.allocator.free(email); + } + self.allocator.free(self.email_recipients); + + if (self.smtp.host.len > 0) self.allocator.free(self.smtp.host); + if (self.smtp.username.len > 0) self.allocator.free(self.smtp.username); + if (self.smtp.password.len > 0) self.allocator.free(self.smtp.password); + if (self.smtp.from.len > 0) self.allocator.free(self.smtp.from); + + if (self.telegram.bot_token.len > 0) self.allocator.free(self.telegram.bot_token); + if (self.telegram.chat_id.len > 0) self.allocator.free(self.telegram.chat_id); + } +}; + +/// Nombre del archivo de configuraci贸n por defecto. +pub const DEFAULT_CONFIG_FILE = "services.conf"; + +/// Carga la configuraci贸n desde un archivo. /// -/// En el futuro, esta funci贸n podr铆a leer desde un archivo JSON -/// para permitir configuraci贸n din谩mica sin recompilar. -pub fn getServices() []const Service { - return &services; +/// Si el archivo no existe, retorna la configuraci贸n por defecto. +pub fn loadFromFile(allocator: std.mem.Allocator, path: []const u8) !Config { + const file = std.fs.cwd().openFile(path, .{}) catch |err| { + if (err == error.FileNotFound) { + return getDefaultConfig(allocator); + } + return err; + }; + defer file.close(); + + return parseConfigFile(allocator, file); +} + +/// Parsea el contenido de un archivo de configuraci贸n. +fn parseConfigFile(allocator: std.mem.Allocator, file: std.fs.File) !Config { + var services = std.ArrayList(Service).init(allocator); + var emails = std.ArrayList([]const u8).init(allocator); + var smtp = SmtpConfig{}; + var telegram = TelegramConfig{}; + + const reader = file.reader(); + var buf: [1024]u8 = undefined; + + while (reader.readUntilDelimiterOrEof(&buf, '\n')) |maybe_line| { + if (maybe_line) |line| { + const trimmed = std.mem.trim(u8, line, " \t\r"); + + // Ignorar l铆neas vac铆as y comentarios + if (trimmed.len == 0 or trimmed[0] == '#') continue; + + // Parsear l铆nea + var parts = std.mem.splitScalar(u8, trimmed, ','); + + const tipo = parts.next() orelse continue; + + if (std.mem.eql(u8, tipo, "http")) { + const name = parts.next() orelse continue; + const url = parts.next() orelse continue; + + try services.append(.{ + .name = try allocator.dupe(u8, name), + .check_type = .http, + .url = try allocator.dupe(u8, url), + }); + } else if (std.mem.eql(u8, tipo, "tcp")) { + const name = parts.next() orelse continue; + const host = parts.next() orelse continue; + const port_str = parts.next() orelse continue; + + const port = std.fmt.parseInt(u16, port_str, 10) catch continue; + + try services.append(.{ + .name = try allocator.dupe(u8, name), + .check_type = .tcp, + .host = try allocator.dupe(u8, host), + .port = port, + }); + } else if (std.mem.eql(u8, tipo, "email")) { + const email = parts.next() orelse continue; + try emails.append(try allocator.dupe(u8, email)); + } else if (std.mem.eql(u8, tipo, "email_smtp")) { + smtp.host = try allocator.dupe(u8, parts.next() orelse continue); + const port_str = parts.next() orelse "587"; + smtp.port = std.fmt.parseInt(u16, port_str, 10) catch 587; + if (parts.next()) |user| smtp.username = try allocator.dupe(u8, user); + if (parts.next()) |pass| smtp.password = try allocator.dupe(u8, pass); + if (parts.next()) |from| smtp.from = try allocator.dupe(u8, from); + } else if (std.mem.eql(u8, tipo, "telegram")) { + telegram.bot_token = try allocator.dupe(u8, parts.next() orelse continue); + telegram.chat_id = try allocator.dupe(u8, parts.next() orelse continue); + } + } else { + break; + } + } else |_| {} + + return Config{ + .services = try services.toOwnedSlice(), + .email_recipients = try emails.toOwnedSlice(), + .smtp = smtp, + .telegram = telegram, + .allocator = allocator, + }; +} + +/// Retorna la configuraci贸n por defecto (hardcoded). +pub fn getDefaultConfig(allocator: std.mem.Allocator) !Config { + const builtin_services = [_]Service{ + .{ .name = "Forgejo (HTTP)", .check_type = .http, .url = "https://git.reugenio.com" }, + .{ .name = "Forgejo (SSH)", .check_type = .tcp, .host = "git.reugenio.com", .port = 2222 }, + .{ .name = "Simifactu API", .check_type = .http, .url = "https://simifactu.com" }, + .{ .name = "Mundisofa", .check_type = .http, .url = "https://mundisofa.com" }, + .{ .name = "Menzuri", .check_type = .http, .url = "https://menzuri.com" }, + }; + + var services = try allocator.alloc(Service, builtin_services.len); + for (builtin_services, 0..) |svc, i| { + services[i] = .{ + .name = try allocator.dupe(u8, svc.name), + .check_type = svc.check_type, + .url = if (svc.url.len > 0) try allocator.dupe(u8, svc.url) else "", + .host = if (svc.host.len > 0) try allocator.dupe(u8, svc.host) else "", + .port = svc.port, + }; + } + + const empty_emails = try allocator.alloc([]const u8, 0); + + return Config{ + .services = services, + .email_recipients = empty_emails, + .smtp = SmtpConfig{}, + .telegram = TelegramConfig{}, + .allocator = allocator, + }; +} + +// --- Compatibilidad con c贸digo existente --- + +/// Lista de servicios por defecto (hardcoded). +const default_services = [_]Service{ + .{ .name = "Forgejo (HTTP)", .check_type = .http, .url = "https://git.reugenio.com" }, + .{ .name = "Forgejo (SSH)", .check_type = .tcp, .host = "git.reugenio.com", .port = 2222 }, + .{ .name = "Simifactu API", .check_type = .http, .url = "https://simifactu.com" }, + .{ .name = "Mundisofa", .check_type = .http, .url = "https://mundisofa.com" }, + .{ .name = "Menzuri", .check_type = .http, .url = "https://menzuri.com" }, +}; + +/// Retorna la lista de servicios por defecto (para compatibilidad). +pub fn getServices() []const Service { + return &default_services; } diff --git a/src/daemon.zig b/src/daemon.zig new file mode 100644 index 0000000..b985e76 --- /dev/null +++ b/src/daemon.zig @@ -0,0 +1,100 @@ +//! M贸dulo para daemonizaci贸n del proceso +//! +//! Permite ejecutar el monitor como un daemon en background, +//! desvincul谩ndolo de la terminal. + +const std = @import("std"); +const linux = std.os.linux; + +/// Errores posibles durante la daemonizaci贸n. +pub const DaemonError = error{ + /// Error al hacer fork del proceso. + ForkFailed, + /// Error al crear nueva sesi贸n. + SetsidFailed, + /// Error al cambiar directorio. + ChdirFailed, +}; + +/// Convierte el proceso actual en un daemon. +/// +/// El proceso se desvincula de la terminal, crea una nueva sesi贸n, +/// y redirige stdin/stdout/stderr a /dev/null. +/// +/// Retorna: +/// - true si somos el proceso hijo (daemon) y debemos continuar +/// - false si somos el proceso padre y debemos salir +/// - error si algo falla +pub fn daemonize() DaemonError!bool { + // Primer fork: separarse del proceso padre + const pid1 = std.posix.fork() catch return DaemonError.ForkFailed; + + if (pid1 != 0) { + // Proceso padre: salir + return false; + } + + // Proceso hijo: crear nueva sesi贸n usando syscall directo + const ret = linux.syscall0(.setsid); + if (@as(isize, @bitCast(ret)) < 0) { + return DaemonError.SetsidFailed; + } + + // Segundo fork: evitar reconectar a terminal + const pid2 = std.posix.fork() catch return DaemonError.ForkFailed; + + if (pid2 != 0) { + // Primer hijo: salir + std.posix.exit(0); + } + + // Daemon real: cambiar a directorio ra铆z para no bloquear unmounts + std.posix.chdir("/") catch return DaemonError.ChdirFailed; + + // Cerrar descriptores est谩ndar y redirigir a /dev/null + const dev_null = std.fs.openFileAbsolute("/dev/null", .{ .mode = .read_write }) catch { + return true; + }; + defer dev_null.close(); + + const null_fd = dev_null.handle; + + // Redirigir stdin, stdout, stderr a /dev/null + std.posix.dup2(null_fd, 0) catch {}; + std.posix.dup2(null_fd, 1) catch {}; + std.posix.dup2(null_fd, 2) catch {}; + + return true; +} + +/// Escribe el PID del daemon a un archivo. +pub fn writePidFile(path: []const u8) !void { + const file = try std.fs.cwd().createFile(path, .{}); + defer file.close(); + + const pid = linux.getpid(); + var buf: [16]u8 = undefined; + const pid_str = std.fmt.bufPrint(&buf, "{d}\n", .{pid}) catch return; + _ = try file.write(pid_str); +} + +/// Lee el PID de un archivo pidfile. +pub fn readPidFile(path: []const u8) !i32 { + const file = try std.fs.cwd().openFile(path, .{}); + defer file.close(); + + var buf: [16]u8 = undefined; + const bytes_read = try file.read(&buf); + + var end: usize = bytes_read; + while (end > 0 and (buf[end - 1] == '\n' or buf[end - 1] == '\r')) { + end -= 1; + } + + return std.fmt.parseInt(i32, buf[0..end], 10) catch error.InvalidPidFile; +} + +/// Elimina el archivo pidfile. +pub fn removePidFile(path: []const u8) void { + std.fs.cwd().deleteFile(path) catch {}; +} diff --git a/src/main.zig b/src/main.zig index 6c9d1c4..74a5dd0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,39 +1,45 @@ //! Service Monitor - Monitor de servicios HTTP y TCP //! -//! Herramienta para verificar que los servicios en el servidor Hetzner (Simba) -//! est茅n funcionando correctamente. Dise帽ado para ser simple, ligero y sin -//! dependencias externas. +//! Herramienta para verificar que los servicios est茅n funcionando correctamente. +//! Dise帽ado para ser simple, ligero y sin dependencias externas. //! //! Uso: -//! zig build run - Verificar todos los servicios una vez -//! zig build run -- --watch - Modo continuo (cada 60s por defecto) -//! zig build run -- --watch -i 30 - Modo continuo cada 30 segundos -//! zig build run -- --log - Guardar log a archivo -//! zig build run -- --notify - Notificaciones desktop en errores -//! zig build run -- --help - Mostrar ayuda +//! service-monitor - Verificar una vez +//! service-monitor --watch - Modo continuo (cada 60s) +//! service-monitor --daemon - Ejecutar como daemon en background +//! service-monitor --config FILE - Usar archivo de configuraci贸n +//! service-monitor --help - Mostrar ayuda const std = @import("std"); const http = @import("http.zig"); const tcp = @import("tcp.zig"); const config = @import("config.zig"); const notify = @import("notify.zig"); +const daemon = @import("daemon.zig"); -/// Nombre del archivo de log por defecto. +/// Archivo de log por defecto. const DEFAULT_LOG_FILE = "service-monitor.log"; +/// Archivo PID para daemon. +const DEFAULT_PID_FILE = "service-monitor.pid"; + /// Opciones de l铆nea de comandos. const Options = struct { /// Modo watch: ejecutar continuamente. watch: bool = false, - /// Intervalo entre checks en segundos (solo en modo watch). + /// Intervalo entre checks en segundos. interval_seconds: u32 = 60, /// Guardar log a archivo. log_to_file: bool = false, /// Ruta del archivo de log. log_file: []const u8 = DEFAULT_LOG_FILE, - /// Activar notificaciones desktop (notify-send). + /// Activar notificaciones desktop. notify: bool = false, - /// Mostrar ayuda y salir. + /// Ejecutar como daemon. + daemonize: bool = false, + /// Archivo de configuraci贸n. + config_file: []const u8 = config.DEFAULT_CONFIG_FILE, + /// Mostrar ayuda. help: bool = false, }; @@ -56,41 +62,78 @@ pub fn main() !void { return; } - // Abrir archivo de log si est谩 habilitado + // Cargar configuraci贸n + var cfg = config.loadFromFile(allocator, options.config_file) catch |err| { + try stdout.print("Error cargando configuraci贸n '{s}': {}\n", .{ options.config_file, err }); + std.process.exit(1); + }; + defer cfg.deinit(); + + // Daemonizar si se solicita + if (options.daemonize) { + if (!options.watch) { + try stdout.print("Error: --daemon requiere --watch\n", .{}); + std.process.exit(1); + } + + try stdout.print("Iniciando daemon (PID file: {s})...\n", .{DEFAULT_PID_FILE}); + + const is_daemon = daemon.daemonize() catch |err| { + try stdout.print("Error al daemonizar: {}\n", .{err}); + std.process.exit(1); + }; + + if (!is_daemon) { + // Proceso padre: salir + try stdout.print("Daemon iniciado.\n", .{}); + return; + } + + // Proceso daemon: escribir PID + daemon.writePidFile(DEFAULT_PID_FILE) catch {}; + } + + // Abrir archivo de log var log_file: ?std.fs.File = null; defer if (log_file) |f| f.close(); - if (options.log_to_file) { + // En modo daemon, siempre loguear a archivo + if (options.log_to_file or options.daemonize) { log_file = std.fs.cwd().createFile(options.log_file, .{ .truncate = false, }) catch |err| { - try stdout.print("Error abriendo archivo de log '{s}': {}\n", .{ options.log_file, err }); + if (!options.daemonize) { + try stdout.print("Error abriendo log '{s}': {}\n", .{ options.log_file, err }); + } std.process.exit(1); }; - // Posicionar al final para append log_file.?.seekFromEnd(0) catch {}; - try stdout.print("Log: {s}\n", .{options.log_file}); + + if (!options.daemonize) { + try stdout.print("Log: {s}\n", .{options.log_file}); + } } - if (options.notify) { + if (options.notify and !options.daemonize) { try stdout.print("Notificaciones: activadas\n", .{}); } - if (options.watch) { - // Modo watch: loop infinito - try stdout.print("\n=== Service Monitor (watch mode, interval: {d}s) ===\n", .{options.interval_seconds}); - try stdout.print("Presiona Ctrl+C para salir\n\n", .{}); + // Obtener writer de salida (null si daemon) + const output_writer = if (options.daemonize) null else stdout; + + if (options.watch or options.daemonize) { + if (!options.daemonize) { + try stdout.print("\n=== Service Monitor (watch mode, interval: {d}s) ===\n", .{options.interval_seconds}); + try stdout.print("Presiona Ctrl+C para salir\n\n", .{}); + } while (true) { - const had_errors = try runChecks(allocator, stdout, log_file, options.notify); - _ = had_errors; // En modo watch no salimos por errores - + _ = try runChecks(allocator, output_writer, log_file, options.notify, cfg.services); std.time.sleep(@as(u64, options.interval_seconds) * std.time.ns_per_s); } } else { - // Modo 煤nico try stdout.print("\n=== Service Monitor ===\n\n", .{}); - const had_errors = try runChecks(allocator, stdout, log_file, options.notify); + const had_errors = try runChecks(allocator, stdout, log_file, options.notify, cfg.services); if (had_errors) { std.process.exit(1); @@ -99,17 +142,13 @@ pub fn main() !void { } /// Ejecuta verificaci贸n de todos los servicios. -/// -/// Escribe resultados a stdout y opcionalmente a archivo de log. -/// Env铆a notificaciones desktop si est谩 habilitado y hay errores. -/// Retorna true si hubo alg煤n error, false si todos OK. fn runChecks( allocator: std.mem.Allocator, - stdout: anytype, + stdout: ?std.fs.File.Writer, log_file: ?std.fs.File, notify_enabled: bool, + services: []const config.Service, ) !bool { - const services = config.getServices(); var had_errors = false; var error_count: u32 = 0; var error_services: [16][]const u8 = undefined; @@ -135,9 +174,10 @@ fn runChecks( seconds, }; - try stdout.print(timestamp_str ++ "\n", timestamp_args); + if (stdout) |out| { + try out.print(timestamp_str ++ "\n", timestamp_args); + } - // Log writer opcional const log_writer = if (log_file) |f| f.writer() else null; if (log_writer) |lw| { try lw.print(timestamp_str ++ "\n", timestamp_args); @@ -150,7 +190,9 @@ fn runChecks( }; if (result) |time_ms| { - try stdout.print("\x1b[32m鉁揬x1b[0m {s} - OK ({d}ms)\n", .{ service.name, time_ms }); + if (stdout) |out| { + try out.print("\x1b[32m鉁揬x1b[0m {s} - OK ({d}ms)\n", .{ service.name, time_ms }); + } if (log_writer) |lw| { try lw.print("OK {s} ({d}ms)\n", .{ service.name, time_ms }); } @@ -160,30 +202,31 @@ fn runChecks( error_services[error_count] = service.name; error_count += 1; } - try stdout.print("\x1b[31m鉁梊x1b[0m {s} - ERROR: {}\n", .{ service.name, err }); + if (stdout) |out| { + try out.print("\x1b[31m鉁梊x1b[0m {s} - ERROR: {}\n", .{ service.name, err }); + } if (log_writer) |lw| { try lw.print("ERROR {s} - {}\n", .{ service.name, err }); } } } - try stdout.print("\n", .{}); + if (stdout) |out| { + try out.print("\n", .{}); + } if (log_writer) |lw| { try lw.print("\n", .{}); } - // Enviar notificaci贸n si hay errores + // Notificaci贸n desktop si hay errores if (notify_enabled and had_errors) { - // Construir mensaje con servicios fallidos var body_buf: [512]u8 = undefined; var body_len: usize = 0; for (error_services[0..error_count]) |svc_name| { - if (body_len > 0) { - if (body_len + 2 < body_buf.len) { - body_buf[body_len] = '\n'; - body_len += 1; - } + if (body_len > 0 and body_len + 1 < body_buf.len) { + body_buf[body_len] = '\n'; + body_len += 1; } const remaining = body_buf.len - body_len; const to_copy = @min(svc_name.len, remaining); @@ -191,14 +234,7 @@ fn runChecks( body_len += to_copy; } - notify.send( - allocator, - "鈿狅笍 Servicios ca铆dos", - body_buf[0..body_len], - "critical", - ) catch { - // Ignorar errores de notificaci贸n, no son cr铆ticos - }; + notify.send(allocator, "鈿狅笍 Servicios ca铆dos", body_buf[0..body_len], "critical") catch {}; } return had_errors; @@ -207,28 +243,28 @@ fn runChecks( /// Parsea los argumentos de l铆nea de comandos. fn parseArgs() !Options { var options = Options{}; - var args = std.process.args(); - _ = args.skip(); // Saltar nombre del programa + _ = args.skip(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--watch") or std.mem.eql(u8, arg, "-w")) { options.watch = true; } else if (std.mem.eql(u8, arg, "--interval") or std.mem.eql(u8, arg, "-i")) { - const interval_str = args.next() orelse return error.MissingIntervalValue; - options.interval_seconds = std.fmt.parseInt(u32, interval_str, 10) catch { - return error.InvalidIntervalValue; - }; + const val = args.next() orelse return error.MissingIntervalValue; + options.interval_seconds = std.fmt.parseInt(u32, val, 10) catch return error.InvalidIntervalValue; } else if (std.mem.eql(u8, arg, "--log") or std.mem.eql(u8, arg, "-l")) { options.log_to_file = true; - // Comprobar si el siguiente arg es una ruta (no empieza con -) - if (args.next()) |next_arg| { - if (next_arg.len > 0 and next_arg[0] != '-') { - options.log_file = next_arg; + if (args.next()) |next| { + if (next.len > 0 and next[0] != '-') { + options.log_file = next; } } } else if (std.mem.eql(u8, arg, "--notify") or std.mem.eql(u8, arg, "-n")) { options.notify = true; + } else if (std.mem.eql(u8, arg, "--daemon") or std.mem.eql(u8, arg, "-d")) { + options.daemonize = true; + } else if (std.mem.eql(u8, arg, "--config") or std.mem.eql(u8, arg, "-c")) { + options.config_file = args.next() orelse return error.MissingConfigFile; } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { options.help = true; } @@ -241,25 +277,39 @@ fn parseArgs() !Options { fn printUsage(stdout: anytype) !void { try stdout.print( \\ - \\Service Monitor - Verifica servicios HTTP y TCP + \\Service Monitor - Verifica disponibilidad de servicios HTTP y TCP \\ \\USO: \\ service-monitor [opciones] \\ \\OPCIONES: - \\ --watch, -w Modo continuo (ejecuta checks en loop) - \\ --interval, -i Intervalo en segundos entre checks (default: 60) - \\ --log, -l [archivo] Guardar log a archivo (default: service-monitor.log) - \\ --notify, -n Notificaciones desktop cuando hay errores (Linux) - \\ --help, -h Muestra esta ayuda + \\ -w, --watch Modo continuo (ejecuta checks en loop) + \\ -i, --interval Intervalo en segundos entre checks (default: 60) + \\ -l, --log [archivo] Guardar log a archivo (default: service-monitor.log) + \\ -n, --notify Notificaciones desktop cuando hay errores + \\ -d, --daemon Ejecutar como daemon en background (requiere -w) + \\ -c, --config Archivo de configuraci贸n (default: services.conf) + \\ -h, --help Muestra esta ayuda + \\ + \\ARCHIVO DE CONFIGURACI脫N: + \\ Formato CSV simple. Ver services.conf.example para ejemplos. + \\ + \\ # Servicios + \\ http,Nombre,https://url.com + \\ tcp,Nombre,host.com,puerto + \\ + \\ # Notificaciones + \\ email,destinatario@email.com + \\ email_smtp,smtp.host.com,587,usuario,password,remitente + \\ telegram,bot_token,chat_id \\ \\EJEMPLOS: - \\ service-monitor Verificar una vez - \\ service-monitor --watch Verificar cada 60 segundos - \\ service-monitor -w -i 30 Verificar cada 30 segundos - \\ service-monitor --log Guardar a service-monitor.log - \\ service-monitor -w -n Watch + notificaciones en errores - \\ service-monitor -w -l -n Watch + log + notificaciones + \\ service-monitor Verificar una vez + \\ service-monitor -w Verificar cada 60 segundos + \\ service-monitor -w -i 30 Verificar cada 30 segundos + \\ service-monitor -w -d Daemon en background + \\ service-monitor -w -d -l -n Daemon + log + notificaciones + \\ service-monitor -c mi-config.conf Usar config personalizado \\ \\ , .{});