Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| 1397fffeca |
10 changed files with 624 additions and 1924 deletions
4
.env
Normal file
4
.env
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
SERVER_NAME=delemaco
|
||||||
|
SERVER_USER=root
|
||||||
|
MAC_ADDRESS=b8:cb:29:a1:f3:88
|
||||||
|
PORT=8080
|
||||||
|
|
@ -2,4 +2,3 @@ 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,137 +2,205 @@ 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: Build multi-arch Raspberry Pi binaries
|
name: Cross-compile for Raspberry Pi
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.22"
|
go-version: '1.20'
|
||||||
|
|
||||||
- name: Build for ARMv6 (Pi Zero / Pi 1)
|
- name: Build for ARM (Pi Zero, Pi 1 - ARMv6)
|
||||||
run: |
|
run: |
|
||||||
GOOS=linux GOARCH=arm GOARM=6 \
|
GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-s -w" -o wol-server-arm6
|
||||||
go build -ldflags="-s -w" -o wol-server-armv6
|
|
||||||
|
|
||||||
- name: Build for ARM64 (Pi Zero 2 / Pi 3 / Pi 4 / Pi 5)
|
- name: Create systemd service file
|
||||||
run: |
|
run: |
|
||||||
GOOS=linux GOARCH=arm64 \
|
cat > wol-server.service << 'EOL'
|
||||||
go build -ldflags="-s -w" -o wol-server-arm64
|
[Unit]
|
||||||
|
Description=WOL Server Go Application
|
||||||
|
After=network.target
|
||||||
|
|
||||||
- name: Create systemd service file
|
[Service]
|
||||||
run: |
|
User=pi
|
||||||
cat > wol-server.service << 'EOF'
|
WorkingDirectory=/home/pi/wol-server
|
||||||
[Unit]
|
ExecStart=/home/pi/wol-server/wol-server
|
||||||
Description=WOL Server Go Application
|
Restart=always
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
[Install]
|
||||||
Type=simple
|
WantedBy=multi-user.target
|
||||||
User=loke
|
EOL
|
||||||
WorkingDirectory=/home/loke/wol-server
|
|
||||||
ExecStart=/home/loke/wol-server/wol-server
|
|
||||||
Restart=always
|
|
||||||
RestartSec=3
|
|
||||||
|
|
||||||
[Install]
|
- name: Create deployment script
|
||||||
WantedBy=multi-user.target
|
run: |
|
||||||
EOF
|
cat > install.sh << 'EOL'
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
- name: Create install script
|
# Installation directory
|
||||||
run: |
|
INSTALL_DIR=~/wol-server
|
||||||
cat > install.sh << 'EOF'
|
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
INSTALL_DIR="$HOME/wol-server"
|
echo "Creating installation directory..."
|
||||||
|
mkdir -p $INSTALL_DIR
|
||||||
|
mkdir -p $INSTALL_DIR/templates
|
||||||
|
|
||||||
echo "Creating installation directory..."
|
echo "Installing application..."
|
||||||
mkdir -p "$INSTALL_DIR/templates"
|
cp wol-server-arm6 $INSTALL_DIR/wol-server
|
||||||
|
chmod +x $INSTALL_DIR/wol-server
|
||||||
|
|
||||||
ARCH=$(uname -m)
|
echo "Installing template files..."
|
||||||
case "$ARCH" in
|
cp -r templates/* $INSTALL_DIR/templates/
|
||||||
armv6l|armv7l)
|
|
||||||
BIN="wol-server-armv6"
|
|
||||||
;;
|
|
||||||
aarch64)
|
|
||||||
BIN="wol-server-arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported architecture: $ARCH"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "Detected architecture: $ARCH"
|
echo "Installing system service..."
|
||||||
echo "Using binary: $BIN"
|
sudo cp wol-server.service /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable wol-server
|
||||||
|
|
||||||
cp "$BIN" "$INSTALL_DIR/wol-server"
|
# Install required dependencies
|
||||||
chmod +x "$INSTALL_DIR/wol-server"
|
echo "Installing dependencies..."
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y wakeonlan sshpass
|
||||||
|
|
||||||
echo "Installing templates..."
|
# Start the service
|
||||||
cp -r templates/* "$INSTALL_DIR/templates/"
|
echo "Starting service..."
|
||||||
|
sudo systemctl restart wol-server
|
||||||
|
|
||||||
echo "Installing systemd service..."
|
echo "==========================================="
|
||||||
sudo cp wol-server.service /etc/systemd/system/
|
echo "Installation complete!"
|
||||||
sudo systemctl daemon-reload
|
echo "The WOL server is now running at http://$(hostname -I | awk '{print $1}'):8080"
|
||||||
sudo systemctl enable wol-server
|
echo "==========================================="
|
||||||
|
EOL
|
||||||
|
|
||||||
echo "Installing dependencies..."
|
chmod +x install.sh
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y wakeonlan sshpass
|
|
||||||
|
|
||||||
echo "Starting service..."
|
- name: Create README with installation instructions
|
||||||
sudo systemctl restart wol-server
|
run: |
|
||||||
|
cat > INSTALL.md << 'EOL'
|
||||||
|
# WOL Server Installation Guide
|
||||||
|
|
||||||
echo "==========================================="
|
This guide will help you install the Wake-on-LAN server on your Raspberry Pi.
|
||||||
echo "WOL Server installed successfully!"
|
|
||||||
echo "URL: http://$(hostname -I | awk '{print $1}'):8080"
|
|
||||||
echo "==========================================="
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x install.sh
|
## Prerequisites
|
||||||
|
|
||||||
- name: Create release package
|
- Raspberry Pi running Raspberry Pi OS (Raspbian)
|
||||||
run: |
|
- SSH access to your Pi
|
||||||
mkdir -p package
|
- SCP or SFTP capability to transfer files
|
||||||
cp wol-server-armv6 package/
|
|
||||||
cp wol-server-arm64 package/
|
|
||||||
cp wol-server.service package/
|
|
||||||
cp install.sh package/
|
|
||||||
cp -r templates package/
|
|
||||||
|
|
||||||
tar -czf wol-server.tar.gz -C package .
|
## Installation Steps
|
||||||
|
|
||||||
- name: Create Forgejo Release
|
### 1. Transfer Files to Raspberry Pi
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
run: |
|
|
||||||
TAG="v${{ github.run_number }}"
|
|
||||||
API_URL="${{ github.server_url }}/api/v1"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
|
|
||||||
# Create release
|
**Option 1: Using SCP from your computer**
|
||||||
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)
|
|
||||||
|
|
||||||
# Upload asset
|
```bash
|
||||||
curl -s -X POST \
|
# Replace with your Pi's IP address
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
PI_IP=192.168.1.100
|
||||||
"${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=wol-server.tar.gz" \
|
|
||||||
-F "attachment=@wol-server.tar.gz" > /dev/null
|
|
||||||
|
|
||||||
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 }}
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +0,0 @@
|
||||||
.env
|
|
||||||
54
README.md
54
README.md
|
|
@ -7,12 +7,7 @@ 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 with auto-refreshing UI
|
- **Status Monitoring**: Check if your target device is online
|
||||||
- **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
|
||||||
|
|
@ -107,10 +102,6 @@ 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
|
||||||
|
|
@ -131,49 +122,6 @@ 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
|
||||||
|
|
||||||
|
|
|
||||||
204
handlers.go
204
handlers.go
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handle the root route - show status
|
// Handle the root route - show status
|
||||||
|
|
@ -14,32 +13,21 @@ 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 {
|
||||||
|
|
@ -59,14 +47,11 @@ 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)
|
||||||
|
|
@ -75,14 +60,11 @@ 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)
|
||||||
|
|
@ -98,14 +80,11 @@ 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)
|
||||||
|
|
@ -114,53 +93,42 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if shutdown password is set
|
// Show confirmation dialog
|
||||||
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
|
|
||||||
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",
|
||||||
ConfirmShutdown: true,
|
ConfirmShutdown: true,
|
||||||
AskPassword: false, // Make sure we don't ask for password
|
AskPassword: false,
|
||||||
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
|
// Handle password entry for shutdown
|
||||||
// enterPasswordHandler function removed - we now use the password from .env directly
|
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
|
// Handle actual shutdown request
|
||||||
func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -170,20 +138,25 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the password from environment variable
|
// Parse form data to get password
|
||||||
if shutdownPassword == "" {
|
if err := r.ParseForm(); err != nil {
|
||||||
log.Printf("SHUTDOWN_PASSWORD not set in environment, cannot perform shutdown")
|
log.Printf("Error parsing form: %v", err)
|
||||||
// Show error message
|
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{
|
data := StatusData{
|
||||||
Server: serverName,
|
Server: serverName,
|
||||||
Status: "Online",
|
Status: "Online",
|
||||||
Color: "#4caf50",
|
Color: "#4caf50",
|
||||||
IsTestMode: runtime.GOOS == "darwin",
|
IsTestMode: runtime.GOOS == "darwin",
|
||||||
AskPassword: false,
|
AskPassword: true,
|
||||||
ErrorMessage: "SHUTDOWN_PASSWORD not set in environment",
|
ErrorMessage: "Password cannot be empty",
|
||||||
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)
|
||||||
|
|
@ -193,22 +166,19 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if isServerOnline() {
|
if isServerOnline() {
|
||||||
// Shutdown the server using the password from .env file
|
// Shutdown the server
|
||||||
err := shutdownServer(shutdownPassword)
|
err := shutdownServer(password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error shutting down server: %v", err)
|
log.Printf("Error shutting down server: %v", err)
|
||||||
|
|
||||||
// Show error message
|
// 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: false, // No longer asking for password
|
AskPassword: true,
|
||||||
ErrorMessage: "Failed to shutdown server. Please check the password in .env file.",
|
ErrorMessage: "Failed to shutdown server. Please check your password.",
|
||||||
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)
|
||||||
|
|
@ -219,14 +189,11 @@ 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)
|
||||||
|
|
@ -235,14 +202,11 @@ 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,27 +1,21 @@
|
||||||
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() {
|
||||||
|
|
@ -47,15 +41,8 @@ func loadEnvVariables() {
|
||||||
port = envPort
|
port = envPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load refresh interval if set
|
log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s",
|
||||||
if envRefresh := os.Getenv("REFRESH_INTERVAL"); envRefresh != "" {
|
serverName, serverUser, macAddress, port)
|
||||||
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() {
|
||||||
|
|
@ -67,24 +54,13 @@ 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)
|
||||||
// Password is now taken directly from .env file
|
http.HandleFunc("/enter-password", enterPasswordHandler)
|
||||||
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)
|
||||||
|
|
@ -97,364 +73,3 @@ 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
557
utils.go
557
utils.go
|
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
|
|
@ -10,20 +9,8 @@ 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
|
||||||
|
|
@ -33,15 +20,9 @@ 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 {
|
||||||
|
|
@ -68,362 +49,12 @@ 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" {
|
||||||
|
|
@ -432,186 +63,38 @@ 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)
|
||||||
// Check if wakeonlan command exists
|
return cmd.Run()
|
||||||
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)
|
||||||
|
|
||||||
var err error
|
// Add more SSH options to handle potential issues
|
||||||
var stderr bytes.Buffer
|
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
|
// Capture stderr to log any error messages
|
||||||
if _, err := exec.LookPath("sshpass"); err == nil {
|
var stderr bytes.Buffer
|
||||||
log.Println("Using sshpass for authentication")
|
cmd.Stderr = &stderr
|
||||||
log.Printf("Password being used: %s", password)
|
|
||||||
|
|
||||||
// Add more SSH options to handle potential issues
|
err := cmd.Run()
|
||||||
cmd := exec.Command("sshpass", "-p", password, "ssh",
|
if err != nil {
|
||||||
"-o", "StrictHostKeyChecking=no",
|
log.Printf("SSH Error details: %s", stderr.String())
|
||||||
"-o", "UserKnownHostsFile=/dev/null",
|
return fmt.Errorf("SSH command failed: %v - %s", err, stderr.String())
|
||||||
"-o", "LogLevel=ERROR",
|
}
|
||||||
"-o", "ConnectTimeout=10",
|
|
||||||
fmt.Sprintf("%s@%s", serverUser, serverName),
|
|
||||||
"sudo", "-S", "shutdown", "-h", "now")
|
|
||||||
|
|
||||||
// Capture stderr to log any error messages
|
return nil
|
||||||
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