Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79864ace88 | |||
| a2140b5ea8 | |||
| fa9fe6095b |
10 changed files with 1926 additions and 626 deletions
4
.env
4
.env
|
|
@ -1,4 +0,0 @@
|
||||||
SERVER_NAME=delemaco
|
|
||||||
SERVER_USER=root
|
|
||||||
MAC_ADDRESS=b8:cb:29:a1:f3:88
|
|
||||||
PORT=8080
|
|
||||||
|
|
@ -2,3 +2,4 @@ SERVER_NAME=pippo
|
||||||
SERVER_USER=root
|
SERVER_USER=root
|
||||||
MAC_ADDRESS=aa:aa:aa:aa:aa:aa
|
MAC_ADDRESS=aa:aa:aa:aa:aa:aa
|
||||||
PORT=8080
|
PORT=8080
|
||||||
|
SHUTDOWN_PASSWORD="password"
|
||||||
|
|
|
||||||
274
.github/workflows/build.yml
vendored
274
.github/workflows/build.yml
vendored
|
|
@ -2,205 +2,137 @@ name: Build and Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Cross-compile for Raspberry Pi
|
name: Build multi-arch Raspberry Pi binaries
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: "1.22"
|
||||||
|
|
||||||
- name: Build for ARM (Pi Zero, Pi 1 - ARMv6)
|
- name: Build for ARMv6 (Pi Zero / Pi 1)
|
||||||
run: |
|
run: |
|
||||||
GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-s -w" -o wol-server-arm6
|
GOOS=linux GOARCH=arm GOARM=6 \
|
||||||
|
go build -ldflags="-s -w" -o wol-server-armv6
|
||||||
|
|
||||||
- name: Create systemd service file
|
- name: Build for ARM64 (Pi Zero 2 / Pi 3 / Pi 4 / Pi 5)
|
||||||
run: |
|
run: |
|
||||||
cat > wol-server.service << 'EOL'
|
GOOS=linux GOARCH=arm64 \
|
||||||
[Unit]
|
go build -ldflags="-s -w" -o wol-server-arm64
|
||||||
Description=WOL Server Go Application
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
- name: Create systemd service file
|
||||||
User=pi
|
run: |
|
||||||
WorkingDirectory=/home/pi/wol-server
|
cat > wol-server.service << 'EOF'
|
||||||
ExecStart=/home/pi/wol-server/wol-server
|
[Unit]
|
||||||
Restart=always
|
Description=WOL Server Go Application
|
||||||
|
After=network.target
|
||||||
|
|
||||||
[Install]
|
[Service]
|
||||||
WantedBy=multi-user.target
|
Type=simple
|
||||||
EOL
|
User=loke
|
||||||
|
WorkingDirectory=/home/loke/wol-server
|
||||||
|
ExecStart=/home/loke/wol-server/wol-server
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
- name: Create deployment script
|
[Install]
|
||||||
run: |
|
WantedBy=multi-user.target
|
||||||
cat > install.sh << 'EOL'
|
EOF
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Installation directory
|
- name: Create install script
|
||||||
INSTALL_DIR=~/wol-server
|
run: |
|
||||||
|
cat > install.sh << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
echo "Creating installation directory..."
|
INSTALL_DIR="$HOME/wol-server"
|
||||||
mkdir -p $INSTALL_DIR
|
|
||||||
mkdir -p $INSTALL_DIR/templates
|
|
||||||
|
|
||||||
echo "Installing application..."
|
echo "Creating installation directory..."
|
||||||
cp wol-server-arm6 $INSTALL_DIR/wol-server
|
mkdir -p "$INSTALL_DIR/templates"
|
||||||
chmod +x $INSTALL_DIR/wol-server
|
|
||||||
|
|
||||||
echo "Installing template files..."
|
ARCH=$(uname -m)
|
||||||
cp -r templates/* $INSTALL_DIR/templates/
|
case "$ARCH" in
|
||||||
|
armv6l|armv7l)
|
||||||
|
BIN="wol-server-armv6"
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
BIN="wol-server-arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: $ARCH"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
echo "Installing system service..."
|
echo "Detected architecture: $ARCH"
|
||||||
sudo cp wol-server.service /etc/systemd/system/
|
echo "Using binary: $BIN"
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable wol-server
|
|
||||||
|
|
||||||
# Install required dependencies
|
cp "$BIN" "$INSTALL_DIR/wol-server"
|
||||||
echo "Installing dependencies..."
|
chmod +x "$INSTALL_DIR/wol-server"
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y wakeonlan sshpass
|
|
||||||
|
|
||||||
# Start the service
|
echo "Installing templates..."
|
||||||
echo "Starting service..."
|
cp -r templates/* "$INSTALL_DIR/templates/"
|
||||||
sudo systemctl restart wol-server
|
|
||||||
|
|
||||||
echo "==========================================="
|
echo "Installing systemd service..."
|
||||||
echo "Installation complete!"
|
sudo cp wol-server.service /etc/systemd/system/
|
||||||
echo "The WOL server is now running at http://$(hostname -I | awk '{print $1}'):8080"
|
sudo systemctl daemon-reload
|
||||||
echo "==========================================="
|
sudo systemctl enable wol-server
|
||||||
EOL
|
|
||||||
|
|
||||||
chmod +x install.sh
|
echo "Installing dependencies..."
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y wakeonlan sshpass
|
||||||
|
|
||||||
- name: Create README with installation instructions
|
echo "Starting service..."
|
||||||
run: |
|
sudo systemctl restart wol-server
|
||||||
cat > INSTALL.md << 'EOL'
|
|
||||||
# WOL Server Installation Guide
|
|
||||||
|
|
||||||
This guide will help you install the Wake-on-LAN server on your Raspberry Pi.
|
echo "==========================================="
|
||||||
|
echo "WOL Server installed successfully!"
|
||||||
|
echo "URL: http://$(hostname -I | awk '{print $1}'):8080"
|
||||||
|
echo "==========================================="
|
||||||
|
EOF
|
||||||
|
|
||||||
## Prerequisites
|
chmod +x install.sh
|
||||||
|
|
||||||
- Raspberry Pi running Raspberry Pi OS (Raspbian)
|
- name: Create release package
|
||||||
- SSH access to your Pi
|
run: |
|
||||||
- SCP or SFTP capability to transfer files
|
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/
|
||||||
|
|
||||||
## Installation Steps
|
tar -czf wol-server.tar.gz -C package .
|
||||||
|
|
||||||
### 1. Transfer Files to Raspberry Pi
|
- 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 }}"
|
||||||
|
|
||||||
**Option 1: Using SCP from your computer**
|
# 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)
|
||||||
|
|
||||||
```bash
|
# Upload asset
|
||||||
# Replace with your Pi's IP address
|
curl -s -X POST \
|
||||||
PI_IP=192.168.1.100
|
-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
|
||||||
|
|
||||||
# Transfer the installation package
|
echo "Release ${TAG} created with asset wol-server.tar.gz"
|
||||||
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 }}
|
|
||||||
|
|
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.env
|
||||||
54
README.md
54
README.md
|
|
@ -7,7 +7,12 @@ A lightweight web-based Wake-on-LAN control panel designed for Raspberry Pi that
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Simple Web Interface**: Boot and shut down your server with a clean, responsive UI
|
- **Simple Web Interface**: Boot and shut down your server with a clean, responsive UI
|
||||||
- **Status Monitoring**: Check if your target device is online
|
- **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
|
||||||
- **Raspberry Pi Optimized**: Built specifically for ARM processors found in all Raspberry Pi models
|
- **Raspberry Pi Optimized**: Built specifically for ARM processors found in all Raspberry Pi models
|
||||||
- **Secure Shutdown**: Password-protected shutdown functionality
|
- **Secure Shutdown**: Password-protected shutdown functionality
|
||||||
- **Lightweight**: Minimal resource usage ideal for running on even the oldest Pi models
|
- **Lightweight**: Minimal resource usage ideal for running on even the oldest Pi models
|
||||||
|
|
@ -102,6 +107,10 @@ nano ~/wol-server/.env
|
||||||
| `SERVER_USER` | SSH username for shutdown | root |
|
| `SERVER_USER` | SSH username for shutdown | root |
|
||||||
| `MAC_ADDRESS` | MAC address for Wake-on-LAN | aa:bb:cc:dd:ee:ff |
|
| `MAC_ADDRESS` | MAC address for Wake-on-LAN | aa:bb:cc:dd:ee:ff |
|
||||||
| `PORT` | Web interface port | 8080 |
|
| `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:
|
After changing configuration, restart the service:
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -122,6 +131,49 @@ http://your-pi-ip:8080
|
||||||
- **Status Checking**: The interface shows the current status (Online/Offline)
|
- **Status Checking**: The interface shows the current status (Online/Offline)
|
||||||
- **Booting**: Click the "Boot" button to send a WOL magic packet
|
- **Booting**: Click the "Boot" button to send a WOL magic packet
|
||||||
- **Shutting Down**: Click "Shutdown" and enter your SSH password when prompted
|
- **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
|
## Maintenance
|
||||||
|
|
||||||
|
|
|
||||||
208
handlers.go
208
handlers.go
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handle the root route - show status
|
// Handle the root route - show status
|
||||||
|
|
@ -13,21 +14,32 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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()
|
online := isServerOnline()
|
||||||
status := "Online"
|
status := "Online"
|
||||||
color := "#4caf50" // Material green
|
color := "#4caf50" // Material green
|
||||||
if !online {
|
if !online {
|
||||||
status = "Offline"
|
status = "Offline"
|
||||||
color = "#d32f2f" // Material red
|
color = "#d32f2f" // Material red
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current schedule configuration
|
||||||
|
scheduleConfig := GetScheduleConfig()
|
||||||
|
|
||||||
data := StatusData{
|
data := StatusData{
|
||||||
Server: serverName,
|
Server: serverName,
|
||||||
Status: status,
|
Status: status,
|
||||||
Color: color,
|
Color: color,
|
||||||
IsTestMode: runtime.GOOS == "darwin",
|
IsTestMode: runtime.GOOS == "darwin",
|
||||||
AskPassword: false,
|
AskPassword: false,
|
||||||
ErrorMessage: "",
|
ErrorMessage: "",
|
||||||
|
Schedule: scheduleConfig,
|
||||||
|
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
RefreshInterval: refreshInterval,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
|
|
@ -47,11 +59,14 @@ func bootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Display booting status
|
// Display booting status
|
||||||
data := StatusData{
|
data := StatusData{
|
||||||
Server: serverName,
|
Server: serverName,
|
||||||
Status: "Booting",
|
Status: "Booting",
|
||||||
Color: "#607d8b", // Material blue-gray
|
Color: "#607d8b", // Material blue-gray
|
||||||
IsTestMode: runtime.GOOS == "darwin",
|
IsTestMode: runtime.GOOS == "darwin",
|
||||||
AskPassword: false,
|
AskPassword: false,
|
||||||
|
Schedule: GetScheduleConfig(),
|
||||||
|
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
RefreshInterval: refreshInterval,
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||||
|
|
@ -60,11 +75,14 @@ func bootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
} else {
|
} else {
|
||||||
// Server is already online
|
// Server is already online
|
||||||
data := StatusData{
|
data := StatusData{
|
||||||
Server: serverName,
|
Server: serverName,
|
||||||
Status: "Online",
|
Status: "Online",
|
||||||
Color: "#4caf50", // Material green
|
Color: "#4caf50", // Material green
|
||||||
IsTestMode: runtime.GOOS == "darwin",
|
IsTestMode: runtime.GOOS == "darwin",
|
||||||
AskPassword: false,
|
AskPassword: false,
|
||||||
|
Schedule: GetScheduleConfig(),
|
||||||
|
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
RefreshInterval: refreshInterval,
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||||
|
|
@ -80,11 +98,14 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if !online {
|
if !online {
|
||||||
// Server is already offline
|
// Server is already offline
|
||||||
data := StatusData{
|
data := StatusData{
|
||||||
Server: serverName,
|
Server: serverName,
|
||||||
Status: "Offline",
|
Status: "Offline",
|
||||||
Color: "#d32f2f", // Material red
|
Color: "#d32f2f", // Material red
|
||||||
IsTestMode: runtime.GOOS == "darwin",
|
IsTestMode: runtime.GOOS == "darwin",
|
||||||
AskPassword: false,
|
AskPassword: false,
|
||||||
|
Schedule: GetScheduleConfig(),
|
||||||
|
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
RefreshInterval: refreshInterval,
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||||
|
|
@ -93,43 +114,54 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show confirmation dialog
|
// Check if shutdown password is set
|
||||||
data := StatusData{
|
if shutdownPassword == "" {
|
||||||
Server: serverName,
|
// Show error about missing password
|
||||||
Status: "Online",
|
data := StatusData{
|
||||||
Color: "#4caf50", // Material green
|
Server: serverName,
|
||||||
IsTestMode: runtime.GOOS == "darwin",
|
Status: "Online",
|
||||||
ConfirmShutdown: true,
|
Color: "#4caf50", // Material green
|
||||||
AskPassword: false,
|
IsTestMode: runtime.GOOS == "darwin",
|
||||||
}
|
AskPassword: false,
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
ConfirmShutdown: false,
|
||||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
ErrorMessage: "SHUTDOWN_PASSWORD not set in environment. Please set it in the .env file.",
|
||||||
log.Printf("Template render error: %v", err)
|
Schedule: GetScheduleConfig(),
|
||||||
}
|
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
}
|
RefreshInterval: refreshInterval,
|
||||||
|
}
|
||||||
// Handle password entry for shutdown
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
func enterPasswordHandler(w http.ResponseWriter, r *http.Request) {
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||||
if !isServerOnline() {
|
log.Printf("Template render error: %v", err)
|
||||||
// Server is already offline, redirect to home
|
}
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show password entry dialog
|
// Show confirmation dialog - we'll use the password from .env
|
||||||
data := StatusData{
|
data := StatusData{
|
||||||
Server: serverName,
|
Server: serverName,
|
||||||
Status: "Online",
|
Status: "Online",
|
||||||
Color: "#4caf50", // Material green
|
Color: "#4caf50", // Material green
|
||||||
IsTestMode: runtime.GOOS == "darwin",
|
IsTestMode: runtime.GOOS == "darwin",
|
||||||
AskPassword: true,
|
ConfirmShutdown: true,
|
||||||
|
AskPassword: false, // Make sure we don't ask for password
|
||||||
|
Schedule: GetScheduleConfig(),
|
||||||
|
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||||
log.Printf("Template render error: %v", err)
|
log.Printf("Template render error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle shutdown confirmation without password
|
||||||
|
// enterPasswordHandler function removed - we now use the password from .env directly
|
||||||
|
|
||||||
// Handle actual shutdown request
|
// Handle actual shutdown request
|
||||||
func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Only process POST requests for security
|
// Only process POST requests for security
|
||||||
|
|
@ -138,25 +170,20 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse form data to get password
|
// Use the password from environment variable
|
||||||
if err := r.ParseForm(); err != nil {
|
if shutdownPassword == "" {
|
||||||
log.Printf("Error parsing form: %v", err)
|
log.Printf("SHUTDOWN_PASSWORD not set in environment, cannot perform shutdown")
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
// Show error message
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get password from form
|
|
||||||
password := r.FormValue("password")
|
|
||||||
|
|
||||||
if password == "" {
|
|
||||||
// Show password form again with error
|
|
||||||
data := StatusData{
|
data := StatusData{
|
||||||
Server: serverName,
|
Server: serverName,
|
||||||
Status: "Online",
|
Status: "Online",
|
||||||
Color: "#4caf50",
|
Color: "#4caf50",
|
||||||
IsTestMode: runtime.GOOS == "darwin",
|
IsTestMode: runtime.GOOS == "darwin",
|
||||||
AskPassword: true,
|
AskPassword: false,
|
||||||
ErrorMessage: "Password cannot be empty",
|
ErrorMessage: "SHUTDOWN_PASSWORD not set in environment",
|
||||||
|
Schedule: GetScheduleConfig(),
|
||||||
|
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
RefreshInterval: refreshInterval,
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||||
|
|
@ -166,19 +193,22 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if isServerOnline() {
|
if isServerOnline() {
|
||||||
// Shutdown the server
|
// Shutdown the server using the password from .env file
|
||||||
err := shutdownServer(password)
|
err := shutdownServer(shutdownPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error shutting down server: %v", err)
|
log.Printf("Error shutting down server: %v", err)
|
||||||
|
|
||||||
// Show password form again with error
|
// Show error message
|
||||||
data := StatusData{
|
data := StatusData{
|
||||||
Server: serverName,
|
Server: serverName,
|
||||||
Status: "Online",
|
Status: "Online",
|
||||||
Color: "#4caf50",
|
Color: "#4caf50",
|
||||||
IsTestMode: runtime.GOOS == "darwin",
|
IsTestMode: runtime.GOOS == "darwin",
|
||||||
AskPassword: true,
|
AskPassword: false, // No longer asking for password
|
||||||
ErrorMessage: "Failed to shutdown server. Please check your 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,
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||||
|
|
@ -189,11 +219,14 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Display shutting down status
|
// Display shutting down status
|
||||||
data := StatusData{
|
data := StatusData{
|
||||||
Server: serverName,
|
Server: serverName,
|
||||||
Status: "Shutting down",
|
Status: "Shutting down",
|
||||||
Color: "#5d4037", // Material brown
|
Color: "#5d4037", // Material brown
|
||||||
IsTestMode: runtime.GOOS == "darwin",
|
IsTestMode: runtime.GOOS == "darwin",
|
||||||
AskPassword: false,
|
AskPassword: false,
|
||||||
|
Schedule: GetScheduleConfig(),
|
||||||
|
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
RefreshInterval: refreshInterval,
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||||
|
|
@ -202,11 +235,14 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
} else {
|
} else {
|
||||||
// Server is already offline
|
// Server is already offline
|
||||||
data := StatusData{
|
data := StatusData{
|
||||||
Server: serverName,
|
Server: serverName,
|
||||||
Status: "Offline",
|
Status: "Offline",
|
||||||
Color: "#d32f2f", // Material red
|
Color: "#d32f2f", // Material red
|
||||||
IsTestMode: runtime.GOOS == "darwin",
|
IsTestMode: runtime.GOOS == "darwin",
|
||||||
AskPassword: false,
|
AskPassword: false,
|
||||||
|
Schedule: GetScheduleConfig(),
|
||||||
|
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
RefreshInterval: refreshInterval,
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||||
|
|
|
||||||
399
main.go
399
main.go
|
|
@ -1,21 +1,27 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default values
|
// Default values
|
||||||
var (
|
var (
|
||||||
serverName = "server" // Server to ping
|
serverName = "server" // Server to ping
|
||||||
serverUser = "root" // SSH username
|
serverUser = "root" // SSH username
|
||||||
macAddress = "aa:aa:aa:aa:aa:aa" // MAC address of the server
|
macAddress = "aa:aa:aa:aa:aa:aa" // MAC address of the server
|
||||||
port = "8080" // Port to listen on
|
port = "8080" // Port to listen on
|
||||||
|
refreshInterval = 60 // UI refresh interval in seconds
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadEnvVariables() {
|
func loadEnvVariables() {
|
||||||
|
|
@ -41,8 +47,15 @@ func loadEnvVariables() {
|
||||||
port = envPort
|
port = envPort
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s",
|
// Load refresh interval if set
|
||||||
serverName, serverUser, macAddress, port)
|
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() {
|
func main() {
|
||||||
|
|
@ -54,13 +67,24 @@ func main() {
|
||||||
log.Fatalf("Failed to setup template: %v", err)
|
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
|
// Register route handlers
|
||||||
http.HandleFunc("/", indexHandler)
|
http.HandleFunc("/", indexHandler)
|
||||||
http.HandleFunc("/boot", bootHandler)
|
http.HandleFunc("/boot", bootHandler)
|
||||||
http.HandleFunc("/confirm-shutdown", confirmShutdownHandler)
|
http.HandleFunc("/confirm-shutdown", confirmShutdownHandler)
|
||||||
http.HandleFunc("/enter-password", enterPasswordHandler)
|
// Password is now taken directly from .env file
|
||||||
http.HandleFunc("/shutdown", shutdownHandler)
|
http.HandleFunc("/shutdown", shutdownHandler)
|
||||||
|
|
||||||
|
// Schedule API endpoints
|
||||||
|
http.HandleFunc("/api/schedule", scheduleHandler)
|
||||||
|
// API shutdown endpoint
|
||||||
|
http.HandleFunc("/api/shutdown", apiShutdownHandler)
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
listenAddr := fmt.Sprintf(":%s", port)
|
listenAddr := fmt.Sprintf(":%s", port)
|
||||||
log.Printf("Starting WOL Server on http://localhost%s", listenAddr)
|
log.Printf("Starting WOL Server on http://localhost%s", listenAddr)
|
||||||
|
|
@ -73,3 +97,364 @@ func main() {
|
||||||
log.Fatalf("Server failed to start: %v", err)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
9
schedule.json
Normal file
9
schedule.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"startTime": "13:46",
|
||||||
|
"endTime": "13:49",
|
||||||
|
"frequency": "daily",
|
||||||
|
"lastRun": "2025-09-05T13:46:41+02:00",
|
||||||
|
"autoShutdown": true,
|
||||||
|
"startedBySchedule": true
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
557
utils.go
557
utils.go
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
|
|
@ -9,8 +10,20 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"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
|
// StatusData holds data for the HTML template
|
||||||
type StatusData struct {
|
type StatusData struct {
|
||||||
Server string
|
Server string
|
||||||
|
|
@ -20,9 +33,15 @@ type StatusData struct {
|
||||||
ConfirmShutdown bool
|
ConfirmShutdown bool
|
||||||
AskPassword bool
|
AskPassword bool
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
|
Schedule ScheduleConfig
|
||||||
|
LastUpdated string
|
||||||
|
RefreshInterval int
|
||||||
}
|
}
|
||||||
|
|
||||||
var tmpl *template.Template
|
var tmpl *template.Template
|
||||||
|
var scheduleConfig ScheduleConfig
|
||||||
|
var scheduleConfigPath = "schedule.json"
|
||||||
|
var shutdownPassword string // Will be loaded from environment
|
||||||
|
|
||||||
// Setup the HTML template
|
// Setup the HTML template
|
||||||
func setupTemplate() error {
|
func setupTemplate() error {
|
||||||
|
|
@ -49,12 +68,362 @@ func setupTemplate() error {
|
||||||
return fmt.Errorf("failed to parse template: %v", err)
|
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
|
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
|
// Check if server is online
|
||||||
func isServerOnline() bool {
|
func isServerOnline() bool {
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
// macOS and Linux have slightly different ping commands
|
// macOS and Linux have slightly different ping commands
|
||||||
if runtime.GOOS == "darwin" {
|
if runtime.GOOS == "darwin" {
|
||||||
|
|
@ -63,38 +432,186 @@ func isServerOnline() bool {
|
||||||
cmd = exec.Command("ping", "-c", "1", "-W", "1", serverName)
|
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()
|
err := cmd.Run()
|
||||||
return err == nil
|
|
||||||
|
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
|
// Send WOL packet
|
||||||
func sendWakeOnLAN() error {
|
func sendWakeOnLAN() error {
|
||||||
log.Printf("Sending WOL packet to %s (%s)", serverName, macAddress)
|
log.Printf("Sending WOL packet to %s (%s)", serverName, macAddress)
|
||||||
cmd := exec.Command("wakeonlan", macAddress)
|
|
||||||
return cmd.Run()
|
// 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
|
// Shutdown server with password
|
||||||
func shutdownServer(password string) error {
|
func shutdownServer(password string) error {
|
||||||
log.Printf("Sending shutdown command to %s", serverName)
|
log.Printf("Sending shutdown command to %s", serverName)
|
||||||
|
|
||||||
// Add more SSH options to handle potential issues
|
var err error
|
||||||
cmd := exec.Command("sshpass", "-p", password, "ssh",
|
var stderr bytes.Buffer
|
||||||
"-o", "StrictHostKeyChecking=no",
|
|
||||||
"-o", "UserKnownHostsFile=/dev/null",
|
|
||||||
"-o", "LogLevel=ERROR",
|
|
||||||
fmt.Sprintf("%s@%s", serverUser, serverName),
|
|
||||||
"sudo", "-S", "shutdown", "-h", "now")
|
|
||||||
|
|
||||||
// Capture stderr to log any error messages
|
// First try using sshpass with password
|
||||||
var stderr bytes.Buffer
|
if _, err := exec.LookPath("sshpass"); err == nil {
|
||||||
cmd.Stderr = &stderr
|
log.Println("Using sshpass for authentication")
|
||||||
|
log.Printf("Password being used: %s", password)
|
||||||
|
|
||||||
err := cmd.Run()
|
// Add more SSH options to handle potential issues
|
||||||
if err != nil {
|
cmd := exec.Command("sshpass", "-p", password, "ssh",
|
||||||
log.Printf("SSH Error details: %s", stderr.String())
|
"-o", "StrictHostKeyChecking=no",
|
||||||
return fmt.Errorf("SSH command failed: %v - %s", err, stderr.String())
|
"-o", "UserKnownHostsFile=/dev/null",
|
||||||
}
|
"-o", "LogLevel=ERROR",
|
||||||
|
"-o", "ConnectTimeout=10",
|
||||||
|
fmt.Sprintf("%s@%s", serverUser, serverName),
|
||||||
|
"sudo", "-S", "shutdown", "-h", "now")
|
||||||
|
|
||||||
return nil
|
// 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue