diff --git a/.env b/.env new file mode 100644 index 0000000..6641729 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +SERVER_NAME=delemaco +SERVER_USER=root +MAC_ADDRESS=b8:cb:29:a1:f3:88 +PORT=8080 diff --git a/.env.sample b/.env.sample index 39aae63..ea2a15a 100644 --- a/.env.sample +++ b/.env.sample @@ -2,4 +2,3 @@ SERVER_NAME=pippo SERVER_USER=root MAC_ADDRESS=aa:aa:aa:aa:aa:aa PORT=8080 -SHUTDOWN_PASSWORD="password" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3dd69d0..b4a8249 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,137 +2,205 @@ name: Build and Release on: push: - branches: [main] + branches: [ main ] pull_request: - branches: [main] + branches: [ main ] jobs: build: - name: Build multi-arch Raspberry Pi binaries + name: Cross-compile for Raspberry Pi runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: "1.22" + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' - - name: Build for ARMv6 (Pi Zero / Pi 1) - run: | - GOOS=linux GOARCH=arm GOARM=6 \ - go build -ldflags="-s -w" -o wol-server-armv6 + - name: Build for ARM (Pi Zero, Pi 1 - ARMv6) + run: | + GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-s -w" -o wol-server-arm6 - - name: Build for ARM64 (Pi Zero 2 / Pi 3 / Pi 4 / Pi 5) - run: | - GOOS=linux GOARCH=arm64 \ - go build -ldflags="-s -w" -o wol-server-arm64 + - name: Create systemd service file + run: | + cat > wol-server.service << 'EOL' + [Unit] + Description=WOL Server Go Application + After=network.target - - name: Create systemd service file - run: | - cat > wol-server.service << 'EOF' - [Unit] - Description=WOL Server Go Application - After=network.target + [Service] + User=pi + WorkingDirectory=/home/pi/wol-server + ExecStart=/home/pi/wol-server/wol-server + Restart=always - [Service] - Type=simple - User=loke - WorkingDirectory=/home/loke/wol-server - ExecStart=/home/loke/wol-server/wol-server - Restart=always - RestartSec=3 + [Install] + WantedBy=multi-user.target + EOL - [Install] - WantedBy=multi-user.target - EOF + - name: Create deployment script + run: | + cat > install.sh << 'EOL' + #!/bin/bash + set -e - - name: Create install script - run: | - cat > install.sh << 'EOF' - #!/bin/bash - set -e + # Installation directory + INSTALL_DIR=~/wol-server - INSTALL_DIR="$HOME/wol-server" + echo "Creating installation directory..." + mkdir -p $INSTALL_DIR + mkdir -p $INSTALL_DIR/templates - echo "Creating installation directory..." - mkdir -p "$INSTALL_DIR/templates" + echo "Installing application..." + cp wol-server-arm6 $INSTALL_DIR/wol-server + chmod +x $INSTALL_DIR/wol-server - ARCH=$(uname -m) - case "$ARCH" in - armv6l|armv7l) - BIN="wol-server-armv6" - ;; - aarch64) - BIN="wol-server-arm64" - ;; - *) - echo "Unsupported architecture: $ARCH" - exit 1 - ;; - esac + echo "Installing template files..." + cp -r templates/* $INSTALL_DIR/templates/ - echo "Detected architecture: $ARCH" - echo "Using binary: $BIN" + echo "Installing system service..." + sudo cp wol-server.service /etc/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable wol-server - cp "$BIN" "$INSTALL_DIR/wol-server" - chmod +x "$INSTALL_DIR/wol-server" + # Install required dependencies + echo "Installing dependencies..." + sudo apt-get update -qq + sudo apt-get install -y wakeonlan sshpass - echo "Installing templates..." - cp -r templates/* "$INSTALL_DIR/templates/" + # Start the service + echo "Starting service..." + sudo systemctl restart wol-server - echo "Installing systemd service..." - sudo cp wol-server.service /etc/systemd/system/ - sudo systemctl daemon-reload - sudo systemctl enable wol-server + echo "===========================================" + echo "Installation complete!" + echo "The WOL server is now running at http://$(hostname -I | awk '{print $1}'):8080" + echo "===========================================" + EOL - echo "Installing dependencies..." - sudo apt-get update -qq - sudo apt-get install -y wakeonlan sshpass + chmod +x install.sh - echo "Starting service..." - sudo systemctl restart wol-server + - name: Create README with installation instructions + run: | + cat > INSTALL.md << 'EOL' + # WOL Server Installation Guide - echo "===========================================" - echo "WOL Server installed successfully!" - echo "URL: http://$(hostname -I | awk '{print $1}'):8080" - echo "===========================================" - EOF + This guide will help you install the Wake-on-LAN server on your Raspberry Pi. - chmod +x install.sh + ## Prerequisites - - name: Create release package - run: | - mkdir -p package - cp wol-server-armv6 package/ - cp wol-server-arm64 package/ - cp wol-server.service package/ - cp install.sh package/ - cp -r templates package/ + - Raspberry Pi running Raspberry Pi OS (Raspbian) + - SSH access to your Pi + - SCP or SFTP capability to transfer files - tar -czf wol-server.tar.gz -C package . + ## Installation Steps - - name: Create Forgejo Release - if: github.ref == 'refs/heads/main' - run: | - TAG="v${{ github.run_number }}" - API_URL="${{ github.server_url }}/api/v1" - REPO="${{ github.repository }}" + ### 1. Transfer Files to Raspberry Pi - # Create release - RELEASE_ID=$(curl -s -X POST \ - -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ - -H "Content-Type: application/json" \ - "${API_URL}/repos/${REPO}/releases" \ - -d "{\"tag_name\": \"${TAG}\", \"name\": \"Release ${TAG}\", \"draft\": false, \"prerelease\": false}" \ - | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) + **Option 1: Using SCP from your computer** - # Upload asset - curl -s -X POST \ - -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ - "${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=wol-server.tar.gz" \ - -F "attachment=@wol-server.tar.gz" > /dev/null + ```bash + # Replace with your Pi's IP address + PI_IP=192.168.1.100 - echo "Release ${TAG} created with asset wol-server.tar.gz" + # Transfer the installation package + scp wol-server.tar.gz pi@$PI_IP:~/ + ``` + + **Option 2: Using SFTP client** + + Use a tool like FileZilla, WinSCP, or Cyberduck to transfer the `wol-server.tar.gz` file to your Raspberry Pi. + + ### 2. SSH into your Raspberry Pi + + ```bash + ssh pi@192.168.1.100 + ``` + + ### 3. Extract and Install + + ```bash + # Navigate to home directory + cd ~ + + # Extract the archive + tar -xzf wol-server.tar.gz + + # Run the installation script + ./install.sh + ``` + + ### 4. Test the Installation + + Open a web browser and navigate to: + ``` + http://[your-pi-ip]:8080 + ``` + + ## Troubleshooting + + If the service fails to start, check the logs: + ```bash + sudo systemctl status wol-server + ``` + + If template errors occur, ensure the template files were copied correctly: + ```bash + ls -la ~/wol-server/templates/ + ``` + + ## Manual Installation (if needed) + + If you encounter issues with the automated install: + + ```bash + # Create directories + mkdir -p ~/wol-server/templates + + # Copy files manually + cp wol-server-arm6 ~/wol-server/wol-server + chmod +x ~/wol-server/wol-server + cp templates/* ~/wol-server/templates/ + sudo cp wol-server.service /etc/systemd/system/ + + # Install dependencies + sudo apt-get update + sudo apt-get install -y wakeonlan sshpass + + # Enable and start service + sudo systemctl daemon-reload + sudo systemctl enable wol-server + sudo systemctl start wol-server + ``` + EOL + + - name: Create all-in-one package + run: | + # Create a single package with everything needed + mkdir -p package + cp wol-server-arm6 package/ + cp wol-server.service package/ + cp install.sh package/ + cp -r templates package/ + cp INSTALL.md package/ + + # Create the tarball + tar -czf wol-server.tar.gz -C package . + + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v1 + if: github.ref == 'refs/heads/main' + with: + tag_name: v${{ github.run_number }} + name: Release v${{ github.run_number }} + draft: false + prerelease: false + files: | + wol-server.tar.gz + INSTALL.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 4c49bd7..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.env diff --git a/README.md b/README.md index 0e16060..7a4aa40 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,7 @@ A lightweight web-based Wake-on-LAN control panel designed for Raspberry Pi that ## Features - **Simple Web Interface**: Boot and shut down your server with a clean, responsive UI -- **Status Monitoring**: Check if your target device is online with auto-refreshing UI -- **Scheduled Backup Window**: Configure automatic daily, bi-daily, weekly, or monthly server startup and shutdown for backup operations -- **Auto Shutdown**: Shut down the server automatically at the end of the backup window -- **Smart Shutdown Protection**: Only auto-shuts down servers that were started by the scheduler -- **Passwordless Operation**: Uses environment variable for all shutdown operations -- **Multiple Shutdown Methods**: Supports various SSH authentication methods for reliable automatic shutdown +- **Status Monitoring**: Check if your target device is online - **Raspberry Pi Optimized**: Built specifically for ARM processors found in all Raspberry Pi models - **Secure Shutdown**: Password-protected shutdown functionality - **Lightweight**: Minimal resource usage ideal for running on even the oldest Pi models @@ -28,7 +23,7 @@ A lightweight web-based Wake-on-LAN control panel designed for Raspberry Pi that ### Option 1: One-Command Installation -1. **Download the latest release** on your local machine from the [Releases page](https://github.com/thisloke/wol-server/releases) +1. **Download the latest release** on your local machine from the [Releases page](https://github.com/yourusername/wol-server/releases) 2. **Transfer the package to your Raspberry Pi** using SCP: ```bash @@ -107,10 +102,6 @@ nano ~/wol-server/.env | `SERVER_USER` | SSH username for shutdown | root | | `MAC_ADDRESS` | MAC address for Wake-on-LAN | aa:bb:cc:dd:ee:ff | | `PORT` | Web interface port | 8080 | -| `SHUTDOWN_PASSWORD` | Password for all shutdown operations | None | -| `REFRESH_INTERVAL` | UI refresh interval in seconds | 60 | - -The scheduled backup window configuration is stored in `schedule.json` in the installation directory. It includes the start time, end time, and frequency settings. After changing configuration, restart the service: ```bash @@ -131,49 +122,6 @@ http://your-pi-ip:8080 - **Status Checking**: The interface shows the current status (Online/Offline) - **Booting**: Click the "Boot" button to send a WOL magic packet - **Shutting Down**: Click "Shutdown" and enter your SSH password when prompted -- **Scheduled Backup Window**: Configure automatic server startup and shutdown on a regular schedule - -#### Using Scheduled Backup Window - -1. Click "Configure Schedule" in the Scheduled Backup Window section -2. Enter your desired start and end times (in 24-hour format) -3. Select a frequency (daily, every 2 days, weekly, or monthly) -4. Optionally, enable "Auto Shutdown" (requires SHUTDOWN_PASSWORD in .env file) -5. Click "Save Schedule" to activate -6. The server will automatically boot at the start time and: - - If auto shutdown is enabled: automatically shut down at the end time - - If auto shutdown is disabled: remain on until manually shut down -7. To modify an existing schedule, click "Edit Schedule" -8. To disable, click "Disable Schedule" from the main interface - -**Note:** All shutdown operations (manual and scheduled) use the SHUTDOWN_PASSWORD from your .env file. - -#### Auto Shutdown Feature - -The auto shutdown feature provides several advantages: -- Saves power by ensuring the server only runs during scheduled backup periods -- Prevents the server from accidentally remaining on after backups are complete -- Fully automates the backup window process -- Smart protection: only shuts down servers that were started by the scheduler - -**Requirements for Shutdown Operations:** -1. Set the `SHUTDOWN_PASSWORD` in your .env file -2. The SSH server must be properly configured on the target server -3. The user account specified in the configuration must have sudo privileges -4. The password must be correct for the specified user account -5. The server must allow password authentication via SSH - -**Troubleshooting Shutdown Operations:** -- If shutdown fails, check the logs for specific error messages -- Ensure `sshpass` is installed on your Raspberry Pi (`sudo apt-get install sshpass`) -- Verify you can manually SSH to the server with the provided credentials -- Confirm the user has sudo privileges to run the shutdown command -- Check if the server requires SSH key authentication instead of password -- Verify the SHUTDOWN_PASSWORD is correctly set in your .env file - -#### Auto-Refreshing UI - -The web interface automatically refreshes every minute (or according to the REFRESH_INTERVAL setting) to show the current server status. This ensures you always see up-to-date information without having to manually refresh the page. ## Maintenance diff --git a/handlers.go b/handlers.go index 684c558..0c4333e 100644 --- a/handlers.go +++ b/handlers.go @@ -4,7 +4,6 @@ import ( "log" "net/http" "runtime" - "time" ) // Handle the root route - show status @@ -14,32 +13,21 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { return } - // 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") - online := isServerOnline() status := "Online" - color := "#4caf50" // Material green + color := "#4caf50" // Material green if !online { status = "Offline" - color = "#d32f2f" // Material red + color = "#d32f2f" // Material red } - // Get current schedule configuration - scheduleConfig := GetScheduleConfig() - data := StatusData{ - Server: serverName, - Status: status, - Color: color, - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - ErrorMessage: "", - Schedule: scheduleConfig, - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: status, + Color: color, + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, + ErrorMessage: "", } if err := tmpl.Execute(w, data); err != nil { @@ -59,14 +47,11 @@ func bootHandler(w http.ResponseWriter, r *http.Request) { // Display booting status data := StatusData{ - Server: serverName, - Status: "Booting", - Color: "#607d8b", // Material blue-gray - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Booting", + Color: "#607d8b", // Material blue-gray + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -75,14 +60,11 @@ func bootHandler(w http.ResponseWriter, r *http.Request) { } else { // Server is already online data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", // Material green - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Online", + Color: "#4caf50", // Material green + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -98,14 +80,11 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) { if !online { // Server is already offline data := StatusData{ - Server: serverName, - Status: "Offline", - Color: "#d32f2f", // Material red - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Offline", + Color: "#d32f2f", // Material red + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -114,53 +93,42 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) { return } - // Check if shutdown password is set - if shutdownPassword == "" { - // Show error about missing password - data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", // Material green - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - ConfirmShutdown: false, - ErrorMessage: "SHUTDOWN_PASSWORD not set in environment. Please set it in the .env file.", - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, - } - if err := tmpl.Execute(w, data); err != nil { - http.Error(w, "Failed to render template", http.StatusInternalServerError) - log.Printf("Template render error: %v", err) - } - return - } - - // Show confirmation dialog - we'll use the password from .env + // Show confirmation dialog data := StatusData{ Server: serverName, Status: "Online", - Color: "#4caf50", // Material green + Color: "#4caf50", // Material green IsTestMode: runtime.GOOS == "darwin", ConfirmShutdown: true, - AskPassword: false, // Make sure we don't ask for password - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), + AskPassword: false, } - - // Notify the user if password is not configured - if shutdownPassword == "" { - data.ErrorMessage = "SHUTDOWN_PASSWORD not set in environment. Shutdown may fail." - } - if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) log.Printf("Template render error: %v", err) } } -// Handle shutdown confirmation without password -// enterPasswordHandler function removed - we now use the password from .env directly +// Handle password entry for shutdown +func enterPasswordHandler(w http.ResponseWriter, r *http.Request) { + if !isServerOnline() { + // Server is already offline, redirect to home + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // Show password entry dialog + data := StatusData{ + Server: serverName, + Status: "Online", + Color: "#4caf50", // Material green + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } +} // Handle actual shutdown request func shutdownHandler(w http.ResponseWriter, r *http.Request) { @@ -170,20 +138,25 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { return } - // Use the password from environment variable - if shutdownPassword == "" { - log.Printf("SHUTDOWN_PASSWORD not set in environment, cannot perform shutdown") - // Show error message + // Parse form data to get password + if err := r.ParseForm(); err != nil { + log.Printf("Error parsing form: %v", err) + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // Get password from form + password := r.FormValue("password") + + if password == "" { + // Show password form again with error data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - ErrorMessage: "SHUTDOWN_PASSWORD not set in environment", - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Online", + Color: "#4caf50", + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + ErrorMessage: "Password cannot be empty", } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -193,22 +166,19 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { } if isServerOnline() { - // Shutdown the server using the password from .env file - err := shutdownServer(shutdownPassword) + // Shutdown the server + err := shutdownServer(password) if err != nil { log.Printf("Error shutting down server: %v", err) - // Show error message + // Show password form again with error data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, // No longer asking for password - ErrorMessage: "Failed to shutdown server. Please check the password in .env file.", - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Online", + Color: "#4caf50", + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + ErrorMessage: "Failed to shutdown server. Please check your password.", } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -219,14 +189,11 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { // Display shutting down status data := StatusData{ - Server: serverName, - Status: "Shutting down", - Color: "#5d4037", // Material brown - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Shutting down", + Color: "#5d4037", // Material brown + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -235,14 +202,11 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { } else { // Server is already offline data := StatusData{ - Server: serverName, - Status: "Offline", - Color: "#d32f2f", // Material red - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Offline", + Color: "#d32f2f", // Material red + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) diff --git a/main.go b/main.go index d4b78c6..7bb50e8 100644 --- a/main.go +++ b/main.go @@ -1,27 +1,21 @@ 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 + 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 ) func loadEnvVariables() { @@ -47,15 +41,8 @@ func loadEnvVariables() { 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) + log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s", + serverName, serverUser, macAddress, port) } func main() { @@ -67,24 +54,13 @@ func main() { 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("/enter-password", enterPasswordHandler) 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) @@ -97,364 +73,3 @@ func main() { 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 - } -} diff --git a/schedule.json b/schedule.json deleted file mode 100644 index a07df7d..0000000 --- a/schedule.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "enabled": true, - "startTime": "13:46", - "endTime": "13:49", - "frequency": "daily", - "lastRun": "2025-09-05T13:46:41+02:00", - "autoShutdown": true, - "startedBySchedule": true -} \ No newline at end of file diff --git a/templates/status.html b/templates/status.html index 1aae2aa..2c00eaa 100644 --- a/templates/status.html +++ b/templates/status.html @@ -1,773 +1,402 @@ - + - - - - + + + Server Status: {{.Server}} - - - + + + - - + +
-
-
-

{{.Status}}

-
Server: {{.Server}}
+
+
+

{{.Status}}

+
Server: {{.Server}}
- -
- -
-

Scheduled Backup Window

- - {{if .Schedule.Enabled}} -
- Automatic scheduled backup is active -
-
-

- Start Time: - {{.Schedule.StartTime}} -

-

- End Time: - {{.Schedule.EndTime}} -

-

- Frequency: - {{.Schedule.Frequency}} -

-

- Auto Shutdown: - {{if .Schedule.AutoShutdown}} - Enabled - {{else}} - Disabled - {{end}} -

-

- Last Update: - {{.LastUpdated}} -

-
-
- - -
- {{else}} -
No active backup schedule
-
- + {{if .IsTestMode}} +
+
+ Running on macOS. Commands will be executed using the provided password. +
{{end}} -
- {{if .IsTestMode}} -
-
- Running on macOS. Commands will be executed using the provided - password. + -
- {{end}} - - -
- - - {{if .ConfirmShutdown}} +
+
+ {{end}} + diff --git a/utils.go b/utils.go index c39d4dd..28e111b 100644 --- a/utils.go +++ b/utils.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "encoding/json" "fmt" "html/template" "log" @@ -10,20 +9,8 @@ import ( "os/exec" "path/filepath" "runtime" - "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 @@ -33,15 +20,9 @@ type StatusData struct { 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 { @@ -68,362 +49,12 @@ func setupTemplate() error { 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 at end time") - - // 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 { - // ONLY consider the server should be on at EXACT start time or EXACT end time - shouldBeOn = (currentTimeStr == scheduleConfig.StartTime) - - // Log that we're waiting for exact times for actions - if currentTimeStr != scheduleConfig.StartTime && currentTimeStr != scheduleConfig.EndTime { - log.Printf("Not at exact schedule times - no action needed until exact start/end time") - } - } - - 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 at exact start time, update the LastRun timestamp - if currentTimeStr == scheduleConfig.StartTime && ShouldRunToday(now) { - // We only track that we've seen the start time - log.Printf("Exact start time reached - marking schedule run") - - // Don't automatically boot the server here - let the main scheduler handle it - // We're just updating state information - scheduleConfig.LastRun = now.Format(time.RFC3339) - if err := saveScheduleConfig(); err != nil { - log.Printf("Warning: 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 { - // We no longer check for windows - we only check at exact times - 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) - } - - // Only log that we're at an exact schedule time - if currentTimeStr == scheduleConfig.StartTime { - log.Println("Currently at exact start time - 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 next run - if scheduleConfig.EndTime != "" && currentTimeStr > scheduleConfig.EndTime { - log.Println("Current time is after end time - resetting for next run") - scheduleConfig.LastRun = "" - scheduleConfig.StartedBySchedule = false // Reset this flag too - 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" { @@ -432,186 +63,38 @@ func isServerOnline() bool { 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 + return err == nil } // 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") - } - } - } + cmd := exec.Command("wakeonlan", macAddress) + return cmd.Run() } // Shutdown server with password func shutdownServer(password string) error { - log.Printf("Sending shutdown command to %s", serverName) + log.Printf("Sending shutdown command to %s", serverName) - var err error - var stderr bytes.Buffer + // 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", + fmt.Sprintf("%s@%s", serverUser, serverName), + "sudo", "-S", "shutdown", "-h", "now") - // 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) + // Capture stderr to log any error messages + var stderr bytes.Buffer + cmd.Stderr = &stderr - // 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") + err := cmd.Run() + if err != nil { + log.Printf("SSH Error details: %s", stderr.String()) + return fmt.Errorf("SSH command failed: %v - %s", err, stderr.String()) + } - // 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) - - 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) - - 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 + return nil }