package main import ( "bytes" "encoding/json" "fmt" "html/template" "log" "os" "os/exec" "path/filepath" "runtime" "strings" "time" ) // ScheduleConfig holds the backup window schedule settings type ScheduleConfig struct { Enabled bool `json:"enabled"` StartTime string `json:"startTime"` // Format: "HH:MM" (24-hour) EndTime string `json:"endTime"` // Format: "HH:MM" (24-hour) Frequency string `json:"frequency"` // "daily", "every2days", "weekly", "monthly" LastRun string `json:"lastRun"` // ISO8601 format - when the schedule last ran AutoShutdown bool `json:"autoShutdown"` // Whether to automatically shut down at end time StartedBySchedule bool `json:"startedBySchedule"` // Whether server was started by scheduler } // StatusData holds data for the HTML template type StatusData struct { Server string Status string Color string IsTestMode bool ConfirmShutdown bool AskPassword bool ErrorMessage string Schedule ScheduleConfig LastUpdated string RefreshInterval int } var tmpl *template.Template var scheduleConfig ScheduleConfig var scheduleConfigPath = "schedule.json" var shutdownPassword string // Will be loaded from environment // Setup the HTML template func setupTemplate() error { // Check if templates directory exists, create if not if _, err := os.Stat("templates"); os.IsNotExist(err) { if err := os.Mkdir("templates", 0755); err != nil { return fmt.Errorf("failed to create templates directory: %v", err) } } // Path to the template file templatePath := filepath.Join("templates", "status.html") // Check if the template file exists if _, err := os.Stat(templatePath); os.IsNotExist(err) { log.Printf("Template file not found at %s. Please create it.", templatePath) return fmt.Errorf("template file not found: %s", templatePath) } // Parse the template from the file var err error tmpl, err = template.ParseFiles(templatePath) if err != nil { return fmt.Errorf("failed to parse template: %v", err) } // Load schedule config err = loadScheduleConfig() if err != nil { log.Printf("Warning: Failed to load schedule config: %v", err) // Continue with default (empty) schedule config } // Check for required system tools checkRequiredTools() // Load shutdown password from environment shutdownPassword = os.Getenv("SHUTDOWN_PASSWORD") if shutdownPassword == "" { log.Println("SHUTDOWN_PASSWORD not set in environment. Automatic shutdown will be disabled.") } else { log.Println("SHUTDOWN_PASSWORD loaded from environment") } return nil } // Check if required system tools are available func checkRequiredTools() { // Check for wakeonlan command if _, err := exec.LookPath("wakeonlan"); err != nil { if _, err := exec.LookPath("etherwake"); err != nil { if _, err := exec.LookPath("wol"); err != nil { log.Printf("WARNING: No Wake-on-LAN tools found. Please install wakeonlan, etherwake, or wol package.") log.Printf("Installation instructions:") log.Printf(" - For Debian/Ubuntu: sudo apt-get install wakeonlan") log.Printf(" - For macOS: brew install wakeonlan") log.Printf(" - For Windows: Download from https://www.depicus.com/wake-on-lan/wake-on-lan-cmd") } else { log.Printf("Using 'wol' for Wake-on-LAN functionality") } } else { log.Printf("Using 'etherwake' for Wake-on-LAN functionality") } } else { log.Printf("Found 'wakeonlan' command for Wake-on-LAN functionality") } // Check for ping command (needed for server status checks) if _, err := exec.LookPath("ping"); err != nil { log.Printf("WARNING: 'ping' command not found. Server status checks may fail.") } } // Load schedule configuration from file func loadScheduleConfig() error { // Check if config file exists if _, err := os.Stat(scheduleConfigPath); os.IsNotExist(err) { // Create default config scheduleConfig = ScheduleConfig{ Enabled: false, StartTime: "", EndTime: "", Frequency: "daily", LastRun: "", AutoShutdown: false, StartedBySchedule: false, } // Save default config return saveScheduleConfig() } // Read the file data, err := os.ReadFile(scheduleConfigPath) if err != nil { return fmt.Errorf("failed to read schedule config file: %v", err) } // Unmarshal JSON data err = json.Unmarshal(data, &scheduleConfig) if err != nil { return fmt.Errorf("failed to parse schedule config: %v", err) } // Log loaded configuration for debugging log.Printf("Loaded schedule config: Enabled=%v, StartTime=%s, EndTime=%s, Frequency=%s", scheduleConfig.Enabled, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency) return nil } // Save schedule configuration to file func saveScheduleConfig() error { data, err := json.MarshalIndent(scheduleConfig, "", " ") if err != nil { return fmt.Errorf("failed to marshal schedule config: %v", err) } err = os.WriteFile(scheduleConfigPath, data, 0644) if err != nil { return fmt.Errorf("failed to save schedule config: %v", err) } return nil } // GetScheduleConfig returns the current schedule config func GetScheduleConfig() ScheduleConfig { return scheduleConfig } // UpdateScheduleConfig updates the schedule configuration func UpdateScheduleConfig(newConfig ScheduleConfig) error { scheduleConfig = newConfig return saveScheduleConfig() } // CheckSchedule checks if server should be on/off based on schedule func CheckSchedule() (shouldBeOn bool) { // If schedule is not enabled, do nothing if !scheduleConfig.Enabled { return false } // If start time or end time is empty, the schedule is not properly configured if scheduleConfig.StartTime == "" || scheduleConfig.EndTime == "" { log.Printf("Schedule configuration incomplete: StartTime=%s, EndTime=%s", scheduleConfig.StartTime, scheduleConfig.EndTime) return false } now := time.Now() today := now.Format("2006-01-02") // Get current time as just hours and minutes for direct string comparison first currentTimeStr := now.Format("15:04") // Log the exact time comparison we're doing log.Printf("Schedule debug: Current=%s, Start=%s, End=%s, LastRun=%s", currentTimeStr, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.LastRun) // Parse start time with proper error handling startTime, err := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.StartTime)) if err != nil { log.Printf("Error parsing start time '%s': %v", scheduleConfig.StartTime, err) return false } // Parse end time with proper error handling endTime, err := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.EndTime)) if err != nil { log.Printf("Error parsing end time '%s': %v", scheduleConfig.EndTime, err) return false } // If end time is before start time, it means the window spans to the next day if endTime.Before(startTime) { endTime = endTime.AddDate(0, 0, 1) // Special case: if we're after midnight but before the end time // we need to adjust the start time to be from yesterday if now.Before(endTime) && now.After(time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())) { startTime = startTime.AddDate(0, 0, -1) } } // Check if the schedule should run today based on frequency // Check if we're in the schedule window if !shouldRunToday(now) { log.Printf("Schedule is active but not set to run today based on frequency: %s", scheduleConfig.Frequency) return false } // Check for auto shutdown at end time if currentTimeStr == scheduleConfig.EndTime { if scheduleConfig.AutoShutdown && shutdownPassword != "" && isServerOnline() { // Only shut down if the server was started by the scheduler if scheduleConfig.StartedBySchedule { log.Printf("Auto shutdown triggered at schedule end time %s", scheduleConfig.EndTime) // Try up to 3 times to shut down the server var shutdownSuccessful bool for attempt := 1; attempt <= 3; attempt++ { log.Printf("Auto shutdown attempt %d/3", attempt) err := shutdownServer(shutdownPassword) if err != nil { log.Printf("Auto shutdown attempt %d failed: %v", attempt, err) if attempt < 3 { time.Sleep(3 * time.Second) } } else { log.Printf("Auto shutdown initiated successfully on attempt %d", attempt) shutdownSuccessful = true break } } if !shutdownSuccessful { log.Printf("All auto shutdown attempts failed") } } else { log.Printf("Server was not started by scheduler, skipping auto shutdown") } } } // Check if current time is within the schedule window // Check if we're between start and end times if currentTimeStr == scheduleConfig.StartTime { log.Printf("Schedule match: Current time exactly matches start time") shouldBeOn = true } else if currentTimeStr == scheduleConfig.EndTime { log.Printf("Schedule end: Current time exactly matches end time") shouldBeOn = false // Check if auto shutdown is enabled if scheduleConfig.AutoShutdown && shutdownPassword != "" && isServerOnline() { // Only shut down if the server was started by the scheduler if scheduleConfig.StartedBySchedule { log.Printf("Auto shutdown is enabled - attempting to shut down server") // Try up to 3 times to shut down the server var shutdownSuccessful bool for attempt := 1; attempt <= 3; attempt++ { log.Printf("Auto shutdown attempt %d/3", attempt) err := shutdownServer(shutdownPassword) if err != nil { log.Printf("Auto shutdown attempt %d failed: %v", attempt, err) if attempt < 3 { time.Sleep(3 * time.Second) } } else { log.Printf("Auto shutdown initiated successfully on attempt %d", attempt) shutdownSuccessful = true break } } if !shutdownSuccessful { log.Printf("All auto shutdown attempts failed") } } else { log.Printf("Server was not started by scheduler, skipping auto shutdown") } } } else { // Otherwise check if we're within the window with a small buffer (30 seconds) shouldBeOn = (now.After(startTime.Add(-30*time.Second)) && now.Before(endTime)) } log.Printf("Schedule window check: Current=%v, Start=%v, End=%v, ShouldBeOn=%v", now.Format("15:04:05"), startTime.Format("15:04:05"), endTime.Format("15:04:05"), shouldBeOn) // Explicitly check end time for better debugging if scheduleConfig.EndTime != "" && currentTimeStr == scheduleConfig.EndTime { log.Printf("EXACT END TIME MATCH! Current time %s equals end time - schedule window should close", currentTimeStr) } // If we're in the schedule window, update the LastRun and attempt to boot the server if shouldBeOn { // Only update LastRun if it hasn't been set yet for this window if scheduleConfig.LastRun == "" { log.Printf("Entering scheduled backup window, updating LastRun timestamp") scheduleConfig.LastRun = now.Format(time.RFC3339) if err := saveScheduleConfig(); err != nil { log.Printf("Warning: Failed to save schedule config: %v", err) } // Force a server boot attempt on window start if !isServerOnline() { log.Printf("Schedule window started - attempting to boot server") if err := sendWakeOnLAN(); err != nil { log.Printf("Schedule boot failed: %v", err) } else { // Mark that server was started by scheduler scheduleConfig.StartedBySchedule = true if err := saveScheduleConfig(); err != nil { log.Printf("Failed to save schedule config: %v", err) } } } } } return shouldBeOn } // shouldRunToday checks if the schedule should run today based on frequency func shouldRunToday(now time.Time) bool { // Special case: if we're within the current window (between start and end time), // we should consider the schedule to be active regardless of LastRun currentTimeStr := now.Format("15:04") today := now.Format("2006-01-02") startTime, startErr := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.StartTime)) endTime, endErr := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.EndTime)) if startErr == nil && endErr == nil { // If end time is before start time, it means the window spans to the next day if endTime.Before(startTime) { endTime = endTime.AddDate(0, 0, 1) } // If we're currently in the window, allow it to run if now.After(startTime) && now.Before(endTime) { log.Println("Currently within today's schedule window - schedule should be active") return true } } // If no previous run, allow it to run if scheduleConfig.LastRun == "" { log.Println("No previous run recorded, schedule can run today") return true } lastRun, err := time.Parse(time.RFC3339, scheduleConfig.LastRun) if err != nil { log.Printf("Error parsing last run date '%s': %v", scheduleConfig.LastRun, err) // If we can't parse the date, better to let it run than to block it return true } // Don't allow running multiple times on the same day unless // it's been reset explicitly (LastRun set to empty) if lastRun.Year() == now.Year() && lastRun.YearDay() == now.YearDay() { // Check if we've passed the end time today - if so, we can reset for tomorrow if scheduleConfig.EndTime != "" && currentTimeStr > scheduleConfig.EndTime { log.Println("Current time is after end time - resetting for next run") scheduleConfig.LastRun = "" saveScheduleConfig() return false } log.Println("Schedule already ran today, skipping") return false } switch scheduleConfig.Frequency { case "daily": // Run every day log.Println("Daily schedule: allowed to run today") return true case "every2days": // Check if at least 2 days have passed elapsed := now.Sub(lastRun) eligible := elapsed >= 48*time.Hour log.Printf("Every 2 days schedule: %v hours elapsed, eligible=%v", elapsed.Hours(), eligible) return eligible case "weekly": // Check if at least 7 days have passed elapsed := now.Sub(lastRun) eligible := elapsed >= 7*24*time.Hour log.Printf("Weekly schedule: %v days elapsed, eligible=%v", elapsed.Hours()/24, eligible) return eligible case "monthly": // Check if last run was in a different month sameMonth := lastRun.Month() == now.Month() && lastRun.Year() == now.Year() log.Printf("Monthly schedule: eligible=%v", !sameMonth) return !sameMonth default: log.Printf("Unknown frequency '%s', defaulting to daily", scheduleConfig.Frequency) return true } } // Check if server is online func isServerOnline() bool { var cmd *exec.Cmd var stdout, stderr bytes.Buffer // macOS and Linux have slightly different ping commands if runtime.GOOS == "darwin" { cmd = exec.Command("ping", "-c", "1", "-W", "1000", serverName) } else { cmd = exec.Command("ping", "-c", "1", "-W", "1", serverName) } // Capture output for debugging cmd.Stdout = &stdout cmd.Stderr = &stderr log.Printf("Checking if server %s is online...", serverName) err := cmd.Run() if err != nil { // Only log the full error in debug mode to avoid spamming the logs if stderr.String() != "" { log.Printf("Server %s is offline: %v - %s", serverName, err, stderr.String()) } else { log.Printf("Server %s is offline", serverName) } return false } log.Printf("Server %s is online", serverName) return true } // Send WOL packet func sendWakeOnLAN() error { log.Printf("Sending WOL packet to %s (%s)", serverName, macAddress) // Check if wakeonlan command exists if _, err := exec.LookPath("wakeonlan"); err == nil { // Create the command cmd := exec.Command("wakeonlan", macAddress) // Capture both stdout and stderr var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr // Execute the command err := cmd.Run() // Log the result if err != nil { log.Printf("WOL command failed: %v - stderr: %s", err, stderr.String()) return fmt.Errorf("WOL command failed: %v - %s", err, stderr.String()) } output := stdout.String() if output != "" { log.Printf("WOL command output: %s", output) } log.Printf("WOL packet sent successfully to %s", macAddress) return nil } else { // wakeonlan command not found, try etherwake if _, err := exec.LookPath("etherwake"); err == nil { log.Printf("Using etherwake as wakeonlan alternative") cmd := exec.Command("etherwake", macAddress) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() if err != nil { log.Printf("etherwake command failed: %v - stderr: %s", err, stderr.String()) return fmt.Errorf("etherwake command failed: %v - %s", err, stderr.String()) } log.Printf("WOL packet sent successfully via etherwake to %s", macAddress) return nil } else { // Try wol command as last resort if _, err := exec.LookPath("wol"); err == nil { log.Printf("Using wol as wakeonlan alternative") cmd := exec.Command("wol", macAddress) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() if err != nil { log.Printf("wol command failed: %v - stderr: %s", err, stderr.String()) return fmt.Errorf("wol command failed: %v - %s", err, stderr.String()) } log.Printf("WOL packet sent successfully via wol to %s", macAddress) return nil } else { // Implement a fallback pure Go WOL solution log.Printf("No WOL tools found. Please install wakeonlan, etherwake, or wol package.") log.Printf("Installation instructions:") log.Printf(" - For Debian/Ubuntu: sudo apt-get install wakeonlan") log.Printf(" - For macOS: brew install wakeonlan") log.Printf(" - For Windows: Download from https://www.depicus.com/wake-on-lan/wake-on-lan-cmd") return fmt.Errorf("wakeonlan command not found in PATH. Please install wakeonlan tool") } } } } // Shutdown server with password func shutdownServer(password string) error { log.Printf("Sending shutdown command to %s", serverName) var err error var stderr bytes.Buffer // First try using sshpass with password if _, err := exec.LookPath("sshpass"); err == nil { log.Println("Using sshpass for authentication") log.Printf("Password being used: %s", password) // Create the command cmdArgs := []string{ "sshpass", "-p", password, "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR", "-o", "ConnectTimeout=10", fmt.Sprintf("%s@%s", serverUser, serverName), "sudo", "-S", "shutdown", "-h", "now", } log.Printf("Full command: %s", strings.Join(cmdArgs, " ")) // Add more SSH options to handle potential issues cmd := exec.Command("sshpass", "-p", password, "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR", "-o", "ConnectTimeout=10", fmt.Sprintf("%s@%s", serverUser, serverName), "sudo", "-S", "shutdown", "-h", "now") // Capture stderr to log any error messages stderr.Reset() cmd.Stderr = &stderr cmd.Stdin = bytes.NewBufferString(password + "\n") err = cmd.Run() if err == nil { log.Println("SSH command executed successfully using sshpass") return nil } log.Printf("sshpass method failed: %v - %s", err, stderr.String()) } // Try direct SSH with password via stdin log.Println("Trying direct SSH with password via stdin") log.Printf("Password being used: %s", password) // Create command args cmdArgs := []string{ "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR", "-o", "ConnectTimeout=10", fmt.Sprintf("%s@%s", serverUser, serverName), "sudo", "-S", "shutdown", "-h", "now", } log.Printf("Full command: %s", strings.Join(cmdArgs, " ")) cmd := exec.Command("ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR", "-o", "ConnectTimeout=10", fmt.Sprintf("%s@%s", serverUser, serverName), "sudo", "-S", "shutdown", "-h", "now") stderr.Reset() cmd.Stderr = &stderr cmd.Stdin = bytes.NewBufferString(password + "\n") err = cmd.Run() if err == nil { log.Println("SSH command executed successfully using direct SSH") return nil } log.Printf("SSH Error details: %s", stderr.String()) // Try a simpler shutdown command as a fallback log.Println("Trying simpler shutdown command as fallback") log.Printf("Password being used: %s", password) // Create command args cmdArgs = []string{ "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR", fmt.Sprintf("%s@%s", serverUser, serverName), "sudo", "shutdown", "now", } log.Printf("Full command: %s", strings.Join(cmdArgs, " ")) cmd = exec.Command("ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR", fmt.Sprintf("%s@%s", serverUser, serverName), "sudo", "shutdown", "now") stderr.Reset() cmd.Stderr = &stderr cmd.Stdin = bytes.NewBufferString(password + "\n") err = cmd.Run() if err != nil { log.Printf("All shutdown attempts failed: %v - %s", err, stderr.String()) return fmt.Errorf("SSH command failed: %v - %s", err, stderr.String()) } return nil }