460 lines
14 KiB
Go
460 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
// Default values
|
|
var (
|
|
serverName = "server" // Server to ping
|
|
serverUser = "root" // SSH username
|
|
macAddress = "aa:aa:aa:aa:aa:aa" // MAC address of the server
|
|
port = "8080" // Port to listen on
|
|
refreshInterval = 60 // UI refresh interval in seconds
|
|
)
|
|
|
|
func loadEnvVariables() {
|
|
// Load .env file if it exists
|
|
if err := godotenv.Load(); err != nil {
|
|
log.Println("No .env file found, using default values")
|
|
}
|
|
|
|
// Override defaults with environment variables if they exist
|
|
if envServerName := os.Getenv("SERVER_NAME"); envServerName != "" {
|
|
serverName = envServerName
|
|
}
|
|
|
|
if envServerUser := os.Getenv("SERVER_USER"); envServerUser != "" {
|
|
serverUser = envServerUser
|
|
}
|
|
|
|
if envMacAddress := os.Getenv("MAC_ADDRESS"); envMacAddress != "" {
|
|
macAddress = envMacAddress
|
|
}
|
|
|
|
if envPort := os.Getenv("PORT"); envPort != "" {
|
|
port = envPort
|
|
}
|
|
|
|
// Load refresh interval if set
|
|
if envRefresh := os.Getenv("REFRESH_INTERVAL"); envRefresh != "" {
|
|
if val, err := strconv.Atoi(envRefresh); err == nil && val > 0 {
|
|
refreshInterval = val
|
|
}
|
|
}
|
|
|
|
log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s, REFRESH=%d",
|
|
serverName, serverUser, macAddress, port, refreshInterval)
|
|
}
|
|
|
|
func main() {
|
|
// Load environment variables
|
|
loadEnvVariables()
|
|
|
|
// Setup template
|
|
if err := setupTemplate(); err != nil {
|
|
log.Fatalf("Failed to setup template: %v", err)
|
|
}
|
|
|
|
// Verify schedule configuration and clean up stale schedule data if needed
|
|
verifyScheduleConfig()
|
|
|
|
// Setup a ticker to check schedule and perform actions
|
|
go runScheduleChecker()
|
|
|
|
// Register route handlers
|
|
http.HandleFunc("/", indexHandler)
|
|
http.HandleFunc("/boot", bootHandler)
|
|
http.HandleFunc("/confirm-shutdown", confirmShutdownHandler)
|
|
// Password is now taken directly from .env file
|
|
http.HandleFunc("/shutdown", shutdownHandler)
|
|
|
|
// Schedule API endpoints
|
|
http.HandleFunc("/api/schedule", scheduleHandler)
|
|
// API shutdown endpoint
|
|
http.HandleFunc("/api/shutdown", apiShutdownHandler)
|
|
|
|
// Start the server
|
|
listenAddr := fmt.Sprintf(":%s", port)
|
|
log.Printf("Starting WOL Server on http://localhost%s", listenAddr)
|
|
|
|
if runtime.GOOS == "darwin" {
|
|
log.Println("Running on macOS - commands will be executed using the provided password")
|
|
}
|
|
|
|
if err := http.ListenAndServe(listenAddr, nil); err != nil {
|
|
log.Fatalf("Server failed to start: %v", err)
|
|
}
|
|
}
|
|
|
|
// API Shutdown handler - shuts down the server with password from environment
|
|
func apiShutdownHandler(w http.ResponseWriter, r *http.Request) {
|
|
// Add cache control headers to prevent caching
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
w.Header().Set("Pragma", "no-cache")
|
|
w.Header().Set("Expires", "0")
|
|
// Set content type for JSON response
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// Only allow POST requests
|
|
if r.Method != "POST" {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": false,
|
|
"error": "Method not allowed. Use POST.",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Check if shutdown password is available in environment
|
|
if shutdownPassword == "" {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": false,
|
|
"error": "SHUTDOWN_PASSWORD not set in environment",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Check if server is online before attempting shutdown
|
|
if !isServerOnline() {
|
|
// Server is already offline
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": false,
|
|
"error": "Server is already offline",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Try to shut down the server using the password from environment
|
|
err := shutdownServer(shutdownPassword)
|
|
if err != nil {
|
|
// Shutdown command failed
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": false,
|
|
"error": "Failed to shutdown server: " + err.Error(),
|
|
})
|
|
log.Printf("API shutdown failed: %v", err)
|
|
return
|
|
}
|
|
|
|
// Shutdown initiated successfully
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"message": "Server shutdown initiated",
|
|
})
|
|
log.Printf("API shutdown successful")
|
|
}
|
|
|
|
// Handle schedule API requests
|
|
func scheduleHandler(w http.ResponseWriter, r *http.Request) {
|
|
// Set content type
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// Handle GET request - return current schedule
|
|
if r.Method == "GET" {
|
|
data, err := json.Marshal(GetScheduleConfig())
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf(`{"error": "Failed to marshal schedule data: %v"}`, err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Write(data)
|
|
return
|
|
}
|
|
|
|
// Handle POST request - update schedule
|
|
if r.Method == "POST" {
|
|
var newConfig ScheduleConfig
|
|
err := json.NewDecoder(r.Body).Decode(&newConfig)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf(`{"error": "Failed to parse request body: %v"}`, err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate the schedule data
|
|
if newConfig.Enabled {
|
|
// Validate time format (HH:MM)
|
|
_, err = time.Parse("15:04", newConfig.StartTime)
|
|
if err != nil {
|
|
http.Error(w, `{"error": "Invalid start time format. Use 24-hour format (HH:MM)"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
_, err = time.Parse("15:04", newConfig.EndTime)
|
|
if err != nil {
|
|
http.Error(w, `{"error": "Invalid end time format. Use 24-hour format (HH:MM)"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate frequency
|
|
validFrequencies := map[string]bool{
|
|
"daily": true,
|
|
"every2days": true,
|
|
"weekly": true,
|
|
"monthly": true,
|
|
}
|
|
|
|
if !validFrequencies[newConfig.Frequency] {
|
|
http.Error(w, `{"error": "Invalid frequency. Use 'daily', 'every2days', 'weekly', or 'monthly'"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Reset lastRun if it wasn't set
|
|
if newConfig.LastRun == "" {
|
|
newConfig.LastRun = ""
|
|
}
|
|
|
|
// If auto shutdown is enabled, make sure we have a password in env
|
|
if newConfig.AutoShutdown && shutdownPassword == "" {
|
|
http.Error(w, `{"error": "SHUTDOWN_PASSWORD not set in environment. Please set it before enabling auto-shutdown"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check if SSH connection can be established with the password
|
|
if newConfig.AutoShutdown && shutdownPassword != "" {
|
|
log.Printf("Testing SSH connection to %s with provided password", serverName)
|
|
|
|
// We'll just check if the server is reachable first
|
|
if !isServerOnline() {
|
|
log.Printf("Server %s is not online, can't test SSH connection", serverName)
|
|
} else {
|
|
// Try to run a harmless command to test SSH connection
|
|
cmd := exec.Command("sshpass", "-p", shutdownPassword, "ssh",
|
|
"-o", "StrictHostKeyChecking=no",
|
|
"-o", "UserKnownHostsFile=/dev/null",
|
|
"-o", "LogLevel=ERROR",
|
|
"-o", "ConnectTimeout=5",
|
|
fmt.Sprintf("%s@%s", serverUser, serverName),
|
|
"echo", "SSH connection test successful")
|
|
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
log.Printf("SSH connection test failed: %v - %s", err, stderr.String())
|
|
// We don't prevent saving the config even if test fails
|
|
// Just log a warning for now
|
|
log.Printf("WARNING: Auto shutdown may not work with the provided password")
|
|
} else {
|
|
log.Printf("SSH connection test successful")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save the new configuration
|
|
err = UpdateScheduleConfig(newConfig)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf(`{"error": "Failed to save schedule config: %v"}`, err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Return the updated config
|
|
data, err := json.Marshal(GetScheduleConfig())
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf(`{"error": "Failed to marshal schedule data: %v"}`, err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Write(data)
|
|
return
|
|
}
|
|
|
|
// Method not allowed
|
|
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
|
}
|
|
|
|
// Verify and clean up schedule configuration
|
|
func verifyScheduleConfig() {
|
|
// If schedule is enabled, validate all required fields
|
|
if scheduleConfig.Enabled {
|
|
log.Println("Verifying schedule configuration...")
|
|
log.Printf("Current config: StartTime=%s, EndTime=%s, Frequency=%s, AutoShutdown=%v",
|
|
scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency, scheduleConfig.AutoShutdown)
|
|
|
|
// Check for valid time formats
|
|
_, startErr := time.Parse("15:04", scheduleConfig.StartTime)
|
|
_, endErr := time.Parse("15:04", scheduleConfig.EndTime)
|
|
|
|
if startErr != nil || endErr != nil || scheduleConfig.StartTime == "" || scheduleConfig.EndTime == "" {
|
|
log.Println("Warning: Invalid time format in schedule configuration, disabling schedule")
|
|
scheduleConfig.Enabled = false
|
|
UpdateScheduleConfig(scheduleConfig)
|
|
return
|
|
}
|
|
|
|
// Immediately check if we need to boot ONLY at exact start time
|
|
now := time.Now()
|
|
currentTimeStr := now.Format("15:04")
|
|
|
|
// Check ONLY if current time EXACTLY matches start time
|
|
if currentTimeStr == scheduleConfig.StartTime && ShouldRunToday(now) {
|
|
log.Printf("STARTUP MATCH: Current time %s matches start time EXACTLY, attempting boot", currentTimeStr)
|
|
if !isServerOnline() {
|
|
sendWakeOnLAN()
|
|
// Mark that the server was started by the scheduler
|
|
scheduleConfig.StartedBySchedule = true
|
|
scheduleConfig.LastRun = now.Format(time.RFC3339)
|
|
UpdateScheduleConfig(scheduleConfig)
|
|
}
|
|
}
|
|
|
|
// Check for valid frequency
|
|
validFrequencies := map[string]bool{
|
|
"daily": true,
|
|
"every2days": true,
|
|
"weekly": true,
|
|
"monthly": true,
|
|
}
|
|
|
|
if !validFrequencies[scheduleConfig.Frequency] {
|
|
log.Println("Warning: Invalid frequency in schedule configuration, setting to daily")
|
|
scheduleConfig.Frequency = "daily"
|
|
UpdateScheduleConfig(scheduleConfig)
|
|
}
|
|
|
|
log.Printf("Schedule configuration verified: Start=%s, End=%s, Frequency=%s",
|
|
scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency)
|
|
}
|
|
}
|
|
|
|
// Run a periodic check of schedule and take appropriate actions
|
|
func runScheduleChecker() {
|
|
// Define the checkScheduleOnce function
|
|
checkScheduleOnce := func() {
|
|
// Only check exact times for schedule actions, don't use window logic
|
|
now := time.Now()
|
|
currentTimeStr := now.Format("15:04")
|
|
serverIsOn := isServerOnline()
|
|
|
|
// Log schedule status (debug level)
|
|
if scheduleConfig.Enabled {
|
|
log.Printf("Schedule check: Current=%s, Start=%s, End=%s, LastRun=%s",
|
|
currentTimeStr, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.LastRun)
|
|
|
|
// Only act at exact start or end times
|
|
// EXACT START TIME MATCH - Try to boot server
|
|
if currentTimeStr == scheduleConfig.StartTime && !serverIsOn && ShouldRunToday(now) {
|
|
log.Println("EXACT START TIME: Initiating boot sequence...")
|
|
|
|
// Try multiple times to boot with small delays between attempts
|
|
for attempt := 1; attempt <= 3; attempt++ {
|
|
log.Printf("Boot attempt %d/3", attempt)
|
|
err := sendWakeOnLAN()
|
|
if err != nil {
|
|
log.Printf("Error booting server from schedule: %v", err)
|
|
} else {
|
|
log.Println("Schedule: Boot command sent successfully")
|
|
// Mark that server was started by scheduler
|
|
scheduleConfig.StartedBySchedule = true
|
|
scheduleConfig.LastRun = now.Format(time.RFC3339)
|
|
UpdateScheduleConfig(scheduleConfig)
|
|
}
|
|
|
|
// Check if server came online
|
|
time.Sleep(3 * time.Second) // Extended wait time for boot check
|
|
if isServerOnline() {
|
|
log.Println("Server successfully booted!")
|
|
break
|
|
}
|
|
|
|
// Short delay before next attempt
|
|
if attempt < 3 {
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}
|
|
// EXACT END TIME MATCH - Try to shutdown server
|
|
} else if currentTimeStr == scheduleConfig.EndTime && serverIsOn {
|
|
// Check if auto-shutdown is enabled
|
|
if scheduleConfig.AutoShutdown && shutdownPassword != "" && scheduleConfig.StartedBySchedule {
|
|
log.Println("EXACT END TIME: Attempting auto-shutdown")
|
|
|
|
// Try multiple 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 {
|
|
// No action at non-exact times, just log status
|
|
if serverIsOn && scheduleConfig.StartedBySchedule && currentTimeStr > scheduleConfig.EndTime {
|
|
log.Printf("Server is still online after end time %s - waiting for next exact end time match", scheduleConfig.EndTime)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update last run timestamp if we've passed the end time
|
|
// This helps track when the schedule was last active
|
|
currentConfig := GetScheduleConfig()
|
|
nowTime := time.Now()
|
|
currentTimeString := nowTime.Format("15:04")
|
|
if currentConfig.Enabled && currentTimeString > currentConfig.EndTime && currentConfig.LastRun != "" {
|
|
lastRun, err := time.Parse(time.RFC3339, currentConfig.LastRun)
|
|
if err == nil {
|
|
// If it's been more than a day since the last update, reset the timestamp
|
|
// This allows the schedule to run again based on frequency
|
|
if time.Since(lastRun) > 24*time.Hour {
|
|
log.Println("Schedule: Resetting last run timestamp for next scheduled run")
|
|
currentConfig.LastRun = ""
|
|
UpdateScheduleConfig(currentConfig)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use a slightly shorter interval for more responsive scheduling
|
|
// First check immediately at startup
|
|
checkScheduleOnce()
|
|
|
|
// Then set up regular checks
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
log.Println("Schedule checker started - checking every 5 seconds")
|
|
log.Printf("Current schedule: enabled=%v, startTime=%s, endTime=%s, frequency=%s, autoShutdown=%v",
|
|
scheduleConfig.Enabled, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency, scheduleConfig.AutoShutdown)
|
|
|
|
for {
|
|
func() {
|
|
// Recover from any panics that might occur in the schedule checker
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Printf("Recovered from panic in schedule checker: %v", r)
|
|
}
|
|
}()
|
|
|
|
checkScheduleOnce()
|
|
}()
|
|
|
|
// Wait for next tick
|
|
<-ticker.C
|
|
}
|
|
}
|