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"
|
|
||||||
|
|
|
||||||
212
.github/workflows/build.yml
vendored
212
.github/workflows/build.yml
vendored
|
|
@ -8,131 +8,199 @@ on:
|
||||||
|
|
||||||
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)
|
|
||||||
run: |
|
|
||||||
GOOS=linux GOARCH=arm64 \
|
|
||||||
go build -ldflags="-s -w" -o wol-server-arm64
|
|
||||||
|
|
||||||
- name: Create systemd service file
|
- name: Create systemd service file
|
||||||
run: |
|
run: |
|
||||||
cat > wol-server.service << 'EOF'
|
cat > wol-server.service << 'EOL'
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=WOL Server Go Application
|
Description=WOL Server Go Application
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
User=pi
|
||||||
User=loke
|
WorkingDirectory=/home/pi/wol-server
|
||||||
WorkingDirectory=/home/loke/wol-server
|
ExecStart=/home/pi/wol-server/wol-server
|
||||||
ExecStart=/home/loke/wol-server/wol-server
|
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOF
|
EOL
|
||||||
|
|
||||||
- name: Create install script
|
- name: Create deployment script
|
||||||
run: |
|
run: |
|
||||||
cat > install.sh << 'EOF'
|
cat > install.sh << 'EOL'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
INSTALL_DIR="$HOME/wol-server"
|
# Installation directory
|
||||||
|
INSTALL_DIR=~/wol-server
|
||||||
|
|
||||||
echo "Creating installation directory..."
|
echo "Creating installation directory..."
|
||||||
mkdir -p "$INSTALL_DIR/templates"
|
mkdir -p $INSTALL_DIR
|
||||||
|
mkdir -p $INSTALL_DIR/templates
|
||||||
|
|
||||||
ARCH=$(uname -m)
|
echo "Installing application..."
|
||||||
case "$ARCH" in
|
cp wol-server-arm6 $INSTALL_DIR/wol-server
|
||||||
armv6l|armv7l)
|
chmod +x $INSTALL_DIR/wol-server
|
||||||
BIN="wol-server-armv6"
|
|
||||||
;;
|
|
||||||
aarch64)
|
|
||||||
BIN="wol-server-arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported architecture: $ARCH"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "Detected architecture: $ARCH"
|
echo "Installing template files..."
|
||||||
echo "Using binary: $BIN"
|
cp -r templates/* $INSTALL_DIR/templates/
|
||||||
|
|
||||||
cp "$BIN" "$INSTALL_DIR/wol-server"
|
echo "Installing system service..."
|
||||||
chmod +x "$INSTALL_DIR/wol-server"
|
|
||||||
|
|
||||||
echo "Installing templates..."
|
|
||||||
cp -r templates/* "$INSTALL_DIR/templates/"
|
|
||||||
|
|
||||||
echo "Installing systemd service..."
|
|
||||||
sudo cp wol-server.service /etc/systemd/system/
|
sudo cp wol-server.service /etc/systemd/system/
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable wol-server
|
sudo systemctl enable wol-server
|
||||||
|
|
||||||
|
# Install required dependencies
|
||||||
echo "Installing dependencies..."
|
echo "Installing dependencies..."
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -y wakeonlan sshpass
|
sudo apt-get install -y wakeonlan sshpass
|
||||||
|
|
||||||
|
# Start the service
|
||||||
echo "Starting service..."
|
echo "Starting service..."
|
||||||
sudo systemctl restart wol-server
|
sudo systemctl restart wol-server
|
||||||
|
|
||||||
echo "==========================================="
|
echo "==========================================="
|
||||||
echo "WOL Server installed successfully!"
|
echo "Installation complete!"
|
||||||
echo "URL: http://$(hostname -I | awk '{print $1}'):8080"
|
echo "The WOL server is now running at http://$(hostname -I | awk '{print $1}'):8080"
|
||||||
echo "==========================================="
|
echo "==========================================="
|
||||||
EOF
|
EOL
|
||||||
|
|
||||||
chmod +x install.sh
|
chmod +x install.sh
|
||||||
|
|
||||||
- name: Create release package
|
- name: Create README with installation instructions
|
||||||
run: |
|
run: |
|
||||||
|
cat > INSTALL.md << 'EOL'
|
||||||
|
# WOL Server Installation Guide
|
||||||
|
|
||||||
|
This guide will help you install the Wake-on-LAN server on your Raspberry Pi.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Raspberry Pi running Raspberry Pi OS (Raspbian)
|
||||||
|
- SSH access to your Pi
|
||||||
|
- SCP or SFTP capability to transfer files
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
### 1. Transfer Files to Raspberry Pi
|
||||||
|
|
||||||
|
**Option 1: Using SCP from your computer**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Replace with your Pi's IP address
|
||||||
|
PI_IP=192.168.1.100
|
||||||
|
|
||||||
|
# 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
|
mkdir -p package
|
||||||
cp wol-server-armv6 package/
|
cp wol-server-arm6 package/
|
||||||
cp wol-server-arm64 package/
|
|
||||||
cp wol-server.service package/
|
cp wol-server.service package/
|
||||||
cp install.sh package/
|
cp install.sh package/
|
||||||
cp -r templates package/
|
cp -r templates package/
|
||||||
|
cp INSTALL.md package/
|
||||||
|
|
||||||
|
# Create the tarball
|
||||||
tar -czf wol-server.tar.gz -C package .
|
tar -czf wol-server.tar.gz -C package .
|
||||||
|
|
||||||
- name: Create Forgejo Release
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
run: |
|
with:
|
||||||
TAG="v${{ github.run_number }}"
|
tag_name: v${{ github.run_number }}
|
||||||
API_URL="${{ github.server_url }}/api/v1"
|
name: Release v${{ github.run_number }}
|
||||||
REPO="${{ github.repository }}"
|
draft: false
|
||||||
|
prerelease: false
|
||||||
# Create release
|
files: |
|
||||||
RELEASE_ID=$(curl -s -X POST \
|
wol-server.tar.gz
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
INSTALL.md
|
||||||
-H "Content-Type: application/json" \
|
env:
|
||||||
"${API_URL}/repos/${REPO}/releases" \
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
-d "{\"tag_name\": \"${TAG}\", \"name\": \"Release ${TAG}\", \"draft\": false, \"prerelease\": false}" \
|
|
||||||
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
|
||||||
|
|
||||||
# Upload asset
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
|
||||||
"${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=wol-server.tar.gz" \
|
|
||||||
-F "attachment=@wol-server.tar.gz" > /dev/null
|
|
||||||
|
|
||||||
echo "Release ${TAG} created with asset wol-server.tar.gz"
|
|
||||||
|
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
||||||
120
handlers.go
120
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,11 +13,6 @@ 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
|
||||||
|
|
@ -27,9 +21,6 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
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,
|
||||||
|
|
@ -37,9 +28,6 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
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 {
|
||||||
|
|
@ -64,9 +52,6 @@ func bootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
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)
|
||||||
|
|
@ -80,9 +65,6 @@ func bootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
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)
|
||||||
|
|
@ -103,9 +85,6 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
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)
|
||||||
|
|
@ -224,9 +194,6 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
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)
|
||||||
|
|
@ -240,9 +207,6 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
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)
|
||||||
|
|
|
||||||
391
main.go
391
main.go
|
|
@ -1,16 +1,11 @@
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
@ -21,7 +16,6 @@ var (
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +1,12 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta
|
|
||||||
http-equiv="refresh"
|
|
||||||
content="{{if .RefreshInterval}}{{.RefreshInterval}}{{else}}60{{end}}"
|
|
||||||
/>
|
|
||||||
<title>Server Status: {{.Server}}</title>
|
<title>Server Status: {{.Server}}</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary-color: {{.Color}};
|
--primary-color: {{.Color}};
|
||||||
|
|
@ -25,8 +18,6 @@
|
||||||
--success-color: #4caf50;
|
--success-color: #4caf50;
|
||||||
--modal-bg: rgba(0, 0, 0, 0.85);
|
--modal-bg: rgba(0, 0, 0, 0.85);
|
||||||
--error-color: #ff6b6b;
|
--error-color: #ff6b6b;
|
||||||
--info-color: #2196f3;
|
|
||||||
--warning-color: #ff9800;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -71,60 +62,6 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule-card {
|
|
||||||
background-color: var(--card-bg);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 30px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-header {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-status {
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 10px;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-status.active {
|
|
||||||
background-color: rgba(76, 175, 80, 0.2);
|
|
||||||
border: 1px solid rgba(76, 175, 80, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-status.inactive {
|
|
||||||
background-color: rgba(244, 67, 54, 0.2);
|
|
||||||
border: 1px solid rgba(244, 67, 54, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-info {
|
|
||||||
margin-bottom: 25px;
|
|
||||||
padding: 15px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-info p {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-info p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-icon {
|
.status-icon {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
@ -218,14 +155,6 @@
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-151 0-255.5-104.5T120-480q0-138 89-239t219-120q20-3 33.5 9.5T480-797q4 20-9 35.5T437-748q-103 12-170 87t-67 181q0 124 88 212t212 88q124 0 212-88t88-212q0-109-69.5-184.5T564-748q-21-3-31.5-19T525-798q3-20 19-30.5t35-6.5q136 19 228.5 122.5T900-480q0 150-104.5 255T480-120Zm0-360Z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-151 0-255.5-104.5T120-480q0-138 89-239t219-120q20-3 33.5 9.5T480-797q4 20-9 35.5T437-748q-103 12-170 87t-67 181q0 124 88 212t212 88q124 0 212-88t88-212q0-109-69.5-184.5T564-748q-21-3-31.5-19T525-798q3-20 19-30.5t35-6.5q136 19 228.5 122.5T900-480q0 150-104.5 255T480-120Zm0-360Z'/%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.schedule::before {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-75 0-140.5-28.5t-114-77q-48.5-48.5-77-114T120-480q0-75 28.5-140.5t77-114q48.5-48.5 114-77T480-840q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-480q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-120Zm0-82q117 0 198.5-81.5T760-482q0-117-81.5-198.5T480-762q-117 0-198.5 81.5T200-482q0 117 81.5 198.5T480-202Zm0-280Zm-40 202-170-170 56-56 114 113 226-226 56 56-282 283Z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.disable::before {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Zm-40 120-56-56 40-40-40-40 56-56 40 40 40-40 56 56-40 40 40 40-56 56-40-40-40 40Z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.submit::before {
|
.button.submit::before {
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z'/%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
|
@ -324,44 +253,12 @@
|
||||||
transition: border-color 0.3s, box-shadow 0.3s;
|
transition: border-color 0.3s, box-shadow 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="datetime-local"] {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input:focus {
|
.form-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: rgba(255, 255, 255, 0.5);
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-checkbox {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
accent-color: var(--success-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-help {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-top: 5px;
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shutdown-warning {
|
|
||||||
margin-top: 15px;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: rgba(255, 152, 0, 0.2);
|
|
||||||
border: 1px solid rgba(255, 152, 0, 0.5);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|
@ -369,25 +266,6 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 50px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge.active {
|
|
||||||
background-color: var(--success-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge.inactive {
|
|
||||||
background-color: var(--danger-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.card {
|
.card {
|
||||||
|
|
@ -465,146 +343,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scheduled Backup Window -->
|
|
||||||
<div class="schedule-card">
|
|
||||||
<h2 class="schedule-header">Scheduled Backup Window</h2>
|
|
||||||
|
|
||||||
{{if .Schedule.Enabled}}
|
|
||||||
<div class="schedule-status active">
|
|
||||||
Automatic scheduled backup is active
|
|
||||||
</div>
|
|
||||||
<div class="schedule-info">
|
|
||||||
<p>
|
|
||||||
<span>Start Time:</span>
|
|
||||||
<span>{{.Schedule.StartTime}}</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span>End Time:</span>
|
|
||||||
<span>{{.Schedule.EndTime}}</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span>Frequency:</span>
|
|
||||||
<span>{{.Schedule.Frequency}}</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span>Auto Shutdown:</span>
|
|
||||||
<span
|
|
||||||
>{{if .Schedule.AutoShutdown}}
|
|
||||||
<span class="badge active">Enabled</span>
|
|
||||||
{{else}}
|
|
||||||
<span class="badge inactive">Disabled</span>
|
|
||||||
{{end}}</span
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span>Last Update:</span>
|
|
||||||
<span>{{.LastUpdated}}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<button id="editSchedule" class="button schedule">
|
|
||||||
Edit Schedule
|
|
||||||
</button>
|
|
||||||
<button id="disableSchedule" class="button disable">
|
|
||||||
Disable Schedule
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="schedule-status inactive">No active backup schedule</div>
|
|
||||||
<div class="controls">
|
|
||||||
<button id="enableSchedule" class="button schedule">
|
|
||||||
Configure Schedule
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{if .IsTestMode}}
|
{{if .IsTestMode}}
|
||||||
<div class="test-panel">
|
<div class="test-panel">
|
||||||
<div class="test-note">
|
<div class="test-note">
|
||||||
Running on macOS. Commands will be executed using the provided
|
Running on macOS. Commands will be executed using the provided password.
|
||||||
password.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="footer">Wake-on-LAN Server Control Panel</div>
|
<div class="footer">
|
||||||
</div>
|
Wake-on-LAN Server Control Panel
|
||||||
|
|
||||||
<!-- Schedule Configuration Modal -->
|
|
||||||
<div id="scheduleModal" class="modal-overlay" style="display: none">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">Configure Backup Schedule</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form id="scheduleForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="startTime" class="form-label">Start Time:</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
id="startTime"
|
|
||||||
name="startTime"
|
|
||||||
class="form-input"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="endTime" class="form-label">End Time:</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
id="endTime"
|
|
||||||
name="endTime"
|
|
||||||
class="form-input"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="frequency" class="form-label">Frequency:</label>
|
|
||||||
<select id="frequency" name="frequency" class="form-input">
|
|
||||||
<option value="daily">Every day</option>
|
|
||||||
<option value="every2days">Every 2 days</option>
|
|
||||||
<option value="weekly">Once a week</option>
|
|
||||||
<option value="monthly">Once a month</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="checkbox-wrapper">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="autoShutdown"
|
|
||||||
name="autoShutdown"
|
|
||||||
class="form-checkbox"
|
|
||||||
/>
|
|
||||||
<label for="autoShutdown"
|
|
||||||
>Enable automatic shutdown at end time</label
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="form-help">
|
|
||||||
When enabled, the server will automatically shut down at the
|
|
||||||
specified end time. Make sure SSH access is properly configured.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-help shutdown-warning">
|
|
||||||
<strong>Note:</strong> To use automatic shutdown, add
|
|
||||||
SHUTDOWN_PASSWORD to your .env file. The server will only be shut
|
|
||||||
down automatically if it was started by the scheduler.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="scheduleError"
|
|
||||||
class="error-message"
|
|
||||||
style="display: none"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button type="button" id="cancelSchedule" class="button">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="button submit">Save Schedule</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -613,161 +361,42 @@
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">Confirm Shutdown</div>
|
<div class="modal-header">Confirm Shutdown</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
Are you sure you want to shut down <strong>{{.Server}}</strong>?<br />
|
Are you sure you want to shut down <strong>{{.Server}}</strong>?<br>
|
||||||
This will immediately power off the server.
|
This will immediately power off the server.
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<a href="/" class="button">Cancel</a>
|
<a href="/" class="button">Cancel</a>
|
||||||
<form action="/shutdown" method="POST" style="display: inline">
|
<a href="/enter-password" class="button danger">Yes, Continue</a>
|
||||||
<button type="submit" class="button danger">Yes, Shutdown</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}} {{if .ErrorMessage}}
|
|
||||||
<div class="modal-overlay">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">Error</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="error-message">{{.ErrorMessage}}</div>
|
|
||||||
|
|
||||||
<div class="shutdown-warning" style="margin-top: 20px">
|
|
||||||
<p>
|
|
||||||
To use the shutdown feature, add SHUTDOWN_PASSWORD to your .env
|
|
||||||
file.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<a href="/" class="button">Back to Home</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<script>
|
{{if .AskPassword}}
|
||||||
// Schedule modal handling
|
<div class="modal-overlay">
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
<div class="modal-content">
|
||||||
const scheduleModal = document.getElementById("scheduleModal");
|
<div class="modal-header">Enter Password</div>
|
||||||
const enableScheduleBtn = document.getElementById("enableSchedule");
|
<div class="modal-body">
|
||||||
const disableScheduleBtn = document.getElementById("disableSchedule");
|
Please enter the password for <strong>{{.Server}}</strong> to shutdown the server.
|
||||||
const cancelScheduleBtn = document.getElementById("cancelSchedule");
|
|
||||||
const scheduleForm = document.getElementById("scheduleForm");
|
|
||||||
const scheduleError = document.getElementById("scheduleError");
|
|
||||||
|
|
||||||
// Show schedule modal
|
<form action="/shutdown" method="POST">
|
||||||
if (enableScheduleBtn) {
|
<div class="form-group">
|
||||||
enableScheduleBtn.addEventListener("click", function () {
|
<label for="password" class="form-label">Password:</label>
|
||||||
scheduleModal.style.display = "flex";
|
<input type="password" id="password" name="password" class="form-input" autofocus>
|
||||||
});
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
// Handle auto shutdown checkbox
|
{{if .ErrorMessage}}
|
||||||
const autoShutdownCheckbox = document.getElementById("autoShutdown");
|
<div class="error-message">{{.ErrorMessage}}</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
// Edit schedule button
|
<div class="modal-actions">
|
||||||
const editScheduleBtn = document.getElementById("editSchedule");
|
<a href="/" class="button">Cancel</a>
|
||||||
if (editScheduleBtn) {
|
<button type="submit" class="button submit">Submit</button>
|
||||||
editScheduleBtn.addEventListener("click", function() {
|
</div>
|
||||||
// Pre-fill form with current values
|
</form>
|
||||||
document.getElementById("startTime").value = "{{.Schedule.StartTime}}";
|
</div>
|
||||||
document.getElementById("endTime").value = "{{.Schedule.EndTime}}";
|
</div>
|
||||||
document.getElementById("frequency").value = "{{.Schedule.Frequency}}";
|
</div>
|
||||||
document.getElementById("autoShutdown").checked = {{.Schedule.AutoShutdown}};
|
{{end}}
|
||||||
|
|
||||||
scheduleModal.style.display = "flex";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide schedule modal
|
|
||||||
if (cancelScheduleBtn) {
|
|
||||||
cancelScheduleBtn.addEventListener("click", function () {
|
|
||||||
scheduleModal.style.display = "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle disable schedule
|
|
||||||
if (disableScheduleBtn) {
|
|
||||||
disableScheduleBtn.addEventListener("click", function () {
|
|
||||||
// Disable schedule via API
|
|
||||||
fetch("/api/schedule", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: false,
|
|
||||||
startTime: "",
|
|
||||||
endTime: "",
|
|
||||||
frequency: "daily",
|
|
||||||
autoShutdown: false,
|
|
||||||
shutdownPassword: "",
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
// Refresh page to show updated status
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error:", error);
|
|
||||||
alert("Failed to disable schedule. Please try again.");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle schedule form submission
|
|
||||||
if (scheduleForm) {
|
|
||||||
scheduleForm.addEventListener("submit", function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// We no longer use these variables - validation happens with startTime/endTime below
|
|
||||||
|
|
||||||
// Get form values
|
|
||||||
const startTime = document.getElementById("startTime").value;
|
|
||||||
const endTime = document.getElementById("endTime").value;
|
|
||||||
const frequency = document.getElementById("frequency").value;
|
|
||||||
|
|
||||||
// Simple validation for times
|
|
||||||
if (!startTime || !endTime) {
|
|
||||||
scheduleError.textContent =
|
|
||||||
"Please enter both start and end times";
|
|
||||||
scheduleError.style.display = "block";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get auto shutdown settings
|
|
||||||
const autoShutdown =
|
|
||||||
document.getElementById("autoShutdown").checked;
|
|
||||||
|
|
||||||
// Submit to API
|
|
||||||
fetch("/api/schedule", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: true,
|
|
||||||
startTime: startTime,
|
|
||||||
endTime: endTime,
|
|
||||||
frequency: frequency,
|
|
||||||
autoShutdown: autoShutdown,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
// Refresh page to show updated status
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error:", error);
|
|
||||||
scheduleError.textContent =
|
|
||||||
"Failed to save schedule. Please try again.";
|
|
||||||
scheduleError.style.display = "block";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
527
utils.go
527
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,184 +63,36 @@ 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)
|
||||||
|
|
||||||
// Check if wakeonlan command exists
|
|
||||||
if _, err := exec.LookPath("wakeonlan"); err == nil {
|
|
||||||
// Create the command
|
|
||||||
cmd := exec.Command("wakeonlan", macAddress)
|
cmd := exec.Command("wakeonlan", macAddress)
|
||||||
|
return cmd.Run()
|
||||||
// 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
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
|
|
||||||
// First try using sshpass with password
|
|
||||||
if _, err := exec.LookPath("sshpass"); err == nil {
|
|
||||||
log.Println("Using sshpass for authentication")
|
|
||||||
log.Printf("Password being used: %s", password)
|
|
||||||
|
|
||||||
// Add more SSH options to handle potential issues
|
// Add more SSH options to handle potential issues
|
||||||
cmd := exec.Command("sshpass", "-p", password, "ssh",
|
cmd := exec.Command("sshpass", "-p", password, "ssh",
|
||||||
"-o", "StrictHostKeyChecking=no",
|
"-o", "StrictHostKeyChecking=no",
|
||||||
"-o", "UserKnownHostsFile=/dev/null",
|
"-o", "UserKnownHostsFile=/dev/null",
|
||||||
"-o", "LogLevel=ERROR",
|
"-o", "LogLevel=ERROR",
|
||||||
"-o", "ConnectTimeout=10",
|
|
||||||
fmt.Sprintf("%s@%s", serverUser, serverName),
|
fmt.Sprintf("%s@%s", serverUser, serverName),
|
||||||
"sudo", "-S", "shutdown", "-h", "now")
|
"sudo", "-S", "shutdown", "-h", "now")
|
||||||
|
|
||||||
// Capture stderr to log any error messages
|
// Capture stderr to log any error messages
|
||||||
stderr.Reset()
|
var stderr bytes.Buffer
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
cmd.Stdin = bytes.NewBufferString(password + "\n")
|
|
||||||
|
|
||||||
err = cmd.Run()
|
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 {
|
if err != nil {
|
||||||
log.Printf("All shutdown attempts failed: %v - %s", err, stderr.String())
|
log.Printf("SSH Error details: %s", stderr.String())
|
||||||
return fmt.Errorf("SSH command failed: %v - %s", err, stderr.String())
|
return fmt.Errorf("SSH command failed: %v - %s", err, stderr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue