Compare commits

..

1 commit
main ... v7

Author SHA1 Message Date
5f87d0accf Initialize Go module 2025-04-21 23:01:59 +02:00
10 changed files with 700 additions and 2094 deletions

4
.env Normal file
View file

@ -0,0 +1,4 @@
SERVER_NAME=delemaco
SERVER_USER=root
MAC_ADDRESS=b8:cb:29:a1:f3:88
PORT=8080

View file

@ -2,4 +2,3 @@ SERVER_NAME=pippo
SERVER_USER=root
MAC_ADDRESS=aa:aa:aa:aa:aa:aa
PORT=8080
SHUTDOWN_PASSWORD="password"

View file

@ -2,137 +2,119 @@ name: Build and Release
on:
push:
branches: [main]
branches: [ main ]
pull_request:
branches: [main]
branches: [ main ]
jobs:
build:
name: Build multi-arch Raspberry Pi binaries
name: Cross-compile for Raspberry Pi
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v4
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: |
GOOS=linux GOARCH=arm GOARM=6 \
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
GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-s -w" -o wol-server-arm6
- name: Create systemd service file
run: |
cat > wol-server.service << 'EOF'
cat > wol-server.service << 'EOL'
[Unit]
Description=WOL Server Go Application
After=network.target
[Service]
Type=simple
User=loke
WorkingDirectory=/home/loke/wol-server
ExecStart=/home/loke/wol-server/wol-server
User=pi
WorkingDirectory=/home/pi/wol-server
ExecStart=/home/pi/wol-server/wol-server
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
EOL
- name: Create install script
- name: Create deployment script
run: |
cat > install.sh << 'EOF'
cat > deploy.sh << 'EOL'
#!/bin/bash
set -e
INSTALL_DIR="$HOME/wol-server"
echo "Creating installation directory..."
mkdir -p "$INSTALL_DIR/templates"
# Detect Raspberry Pi model and use the appropriate binary
MODEL=$(cat /proc/cpuinfo | grep "Model" | sed 's/Model\s*:\s*//g')
echo "Detected model: $MODEL"
# Select the right binary based on processor architecture
ARCH=$(uname -m)
case "$ARCH" in
armv6l|armv7l)
BIN="wol-server-armv6"
;;
aarch64)
BIN="wol-server-arm64"
;;
*)
echo "Unsupported architecture: $ARCH"
exit 1
;;
esac
echo "Architecture: $ARCH"
echo "Detected architecture: $ARCH"
echo "Using binary: $BIN"
if [[ "$ARCH" == "aarch64" ]]; then
echo "Using ARM64 binary"
cp wol-server-arm64 wol-server
elif [[ "$ARCH" == "armv7l" ]]; then
echo "Using ARMv7 binary"
cp wol-server-arm7 wol-server
else
echo "Using ARMv6 binary"
cp wol-server-arm6 wol-server
fi
cp "$BIN" "$INSTALL_DIR/wol-server"
chmod +x "$INSTALL_DIR/wol-server"
echo "Creating directory..."
mkdir -p ~/wol-server
echo "Installing templates..."
cp -r templates/* "$INSTALL_DIR/templates/"
echo "Copying application..."
cp wol-server ~/wol-server/
chmod +x ~/wol-server/wol-server
echo "Installing systemd service..."
echo "Installing service..."
sudo cp wol-server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable wol-server
echo "Installing dependencies..."
sudo apt-get update -qq
sudo apt-get install -y wakeonlan sshpass
echo "Starting service..."
sudo systemctl restart wol-server
echo "==========================================="
echo "WOL Server installed successfully!"
echo "URL: http://$(hostname -I | awk '{print $1}'):8080"
echo "==========================================="
EOF
echo "Deployment complete!"
echo "Service status:"
sudo systemctl status wol-server
EOL
chmod +x install.sh
chmod +x deploy.sh
- name: Create release package
- name: Create archive for each platform
run: |
mkdir -p package
cp wol-server-armv6 package/
cp wol-server-arm64 package/
cp wol-server.service package/
cp install.sh package/
cp -r templates package/
# ARMv6 package
mkdir -p armv6-package
cp wol-server-arm6 armv6-package/wol-server-arm6
cp wol-server.service armv6-package/
cp deploy.sh armv6-package/
tar -czf wol-server-armv6.tar.gz -C armv6-package .
tar -czf wol-server.tar.gz -C package .
# All-in-one package
mkdir -p all-package
cp wol-server-arm6 all-package/
cp wol-server.service all-package/
cp deploy.sh all-package/
tar -czf wol-server-all.tar.gz -C all-package .
- name: Create Forgejo Release
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v1
if: github.ref == 'refs/heads/main'
run: |
TAG="v${{ github.run_number }}"
API_URL="${{ github.server_url }}/api/v1"
REPO="${{ github.repository }}"
# Create release
RELEASE_ID=$(curl -s -X POST \
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
-H "Content-Type: application/json" \
"${API_URL}/repos/${REPO}/releases" \
-d "{\"tag_name\": \"${TAG}\", \"name\": \"Release ${TAG}\", \"draft\": false, \"prerelease\": false}" \
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
# 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"
with:
tag_name: v${{ github.run_number }}
name: Release v${{ github.run_number }}
draft: false
prerelease: false
files: |
wol-server-arm6
wol-server.service
deploy.sh
wol-server-armv6.tar.gz
wol-server-all.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View file

@ -1 +0,0 @@
.env

372
README.md
View file

@ -1,286 +1,226 @@
# WOL Server - Wake-on-LAN Control Panel for Raspberry Pi
# WOL-Server
A lightweight web-based Wake-on-LAN control panel designed for Raspberry Pi that lets you remotely power on and shut down your network devices.
![WOL Server Screenshot](https://i.imgur.com/example.jpg)
A lightweight Wake-on-LAN (WOL) server application designed specifically for Raspberry Pi devices. This tool allows you to remotely power on your network devices using magic packets through a simple, user-friendly interface.
## Features
- **Simple Web Interface**: Boot and shut down your server with a clean, responsive UI
- **Status Monitoring**: Check if your target device is online with auto-refreshing UI
- **Scheduled Backup Window**: Configure automatic daily, bi-daily, weekly, or monthly server startup and shutdown for backup operations
- **Auto Shutdown**: Shut down the server automatically at the end of the backup window
- **Smart Shutdown Protection**: Only auto-shuts down servers that were started by the scheduler
- **Passwordless Operation**: Uses environment variable for all shutdown operations
- **Multiple Shutdown Methods**: Supports various SSH authentication methods for reliable automatic shutdown
- **Raspberry Pi Optimized**: Built specifically for ARM processors found in all Raspberry Pi models
- **Secure Shutdown**: Password-protected shutdown functionality
- **Lightweight**: Minimal resource usage ideal for running on even the oldest Pi models
- **Easy Setup**: Simple installation process with clear instructions
- **Cross-Platform Compatibility**: Optimized for various Raspberry Pi models (Zero, 1, 2, 3, 4)
- **Easy Installation**: Automated deployment script for quick setup
- **Systemd Integration**: Runs as a system service for reliability
- **Simple Web Interface**: Control your devices through an intuitive web UI
- **Configurable**: Easily customize settings through environment variables or `.env` file
- **Low Resource Usage**: Minimal footprint to run efficiently on any Pi
## Installation
### Prerequisites
### Method 1: Download Individual Files (Recommended)
- Raspberry Pi (any model) running Raspberry Pi OS
- Network connection
- Basic knowledge of SSH/terminal
1. Download the necessary files directly to your Raspberry Pi:
### Option 1: One-Command Installation
```bash
# Create a directory for installation
mkdir -p ~/wol-install
cd ~/wol-install
1. **Download the latest release** on your local machine from the [Releases page](https://github.com/thisloke/wol-server/releases)
# Download the executable, service file, and deployment script
wget https://github.com/thisloke/wol-server/releases/download/v6/wol-server-arm6
wget https://github.com/thisloke/wol-server/releases/download/v6/wol-server.service
wget https://github.com/thisloke/wol-server/releases/download/v6/deploy.sh
2. **Transfer the package to your Raspberry Pi** using SCP:
```bash
scp wol-server.tar.gz pi@your-pi-ip:~/
```
# Make files executable
chmod +x wol-server-arm6
chmod +x deploy.sh
```
3. **SSH into your Raspberry Pi**:
```bash
ssh pi@your-pi-ip
```
2. Create a `.env` file for configuration:
```bash
cat > .env << EOL
# Server Configuration
SERVER_NAME=pippo
SERVER_USER=root
MAC_ADDRESS=aa:aa:aa:aa:aa:aa
PORT=8080
EOL
```
4. **Install with a single command**:
```bash
tar -xzf wol-server.tar.gz && ./install.sh
```
3. Run the deployment script:
```bash
./deploy.sh
```
5. **Access the web interface** at:
```
http://your-pi-ip:8080
```
### Method 2: Build from Source
### Option 2: Manual Installation
If you prefer to build the application from source:
If you prefer a manual approach or encounter issues with the automated install:
```bash
# Install Go (if not already installed)
sudo apt update
sudo apt install golang-go
1. **Create installation directory**:
```bash
mkdir -p ~/wol-server/templates
```
# Clone the repository
git clone https://github.com/thisloke/wol-server.git
cd wol-server
2. **Transfer and install program files**:
```bash
# Copy the executable
cp wol-server-arm6 ~/wol-server/wol-server
chmod +x ~/wol-server/wol-server
# Install dependencies
go get github.com/joho/godotenv
# Copy template files
cp templates/* ~/wol-server/templates/
# Create a .env file for configuration
cat > .env << EOL
# Server Configuration
SERVER_NAME=pippo
SERVER_USER=root
MAC_ADDRESS=aa:aa:aa:aa:aa:aa
PORT=8080
EOL
# Create .env file
cat > ~/wol-server/.env << EOL
SERVER_NAME=pippo
SERVER_USER=root
MAC_ADDRESS=aa:bb:cc:dd:ee:ff
PORT=8080
EOL
```
# Build the application
go build -o wol-server
3. **Install service**:
```bash
sudo cp wol-server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable wol-server
sudo systemctl start wol-server
```
# Create installation directory
mkdir -p ~/wol-server
4. **Install required dependencies**:
```bash
sudo apt-get update
sudo apt-get install -y wakeonlan sshpass
```
# Copy the binary and config
cp wol-server ~/wol-server/
cp .env ~/wol-server/
chmod +x ~/wol-server/wol-server
# Create and install systemd service
sudo bash -c 'cat > /etc/systemd/system/wol-server.service << EOL
[Unit]
Description=WOL Server Go Application
After=network.target
[Service]
User=pi
WorkingDirectory=/home/pi/wol-server
ExecStart=/home/pi/wol-server/wol-server
Restart=always
[Install]
WantedBy=multi-user.target
EOL'
# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable wol-server
sudo systemctl start wol-server
```
## Configuration
The application can be configured by editing the `.env` file in the installation directory:
```bash
nano ~/wol-server/.env
```
WOL-server can be configured using environment variables or a `.env` file in the application directory.
### Available Configuration Options
| Setting | Description | Default |
|---------|-------------|---------|
| `SERVER_NAME` | Hostname/IP of target server | pippo |
| `SERVER_USER` | SSH username for shutdown | root |
| `MAC_ADDRESS` | MAC address for Wake-on-LAN | aa:bb:cc:dd:ee:ff |
| `PORT` | Web interface port | 8080 |
| `SHUTDOWN_PASSWORD` | Password for all shutdown operations | None |
| `REFRESH_INTERVAL` | UI refresh interval in seconds | 60 |
| Environment Variable | Description | Default Value |
|----------------------|-------------|---------------|
| `SERVER_NAME` | Name of the server to ping/wake | `pippo` |
| `SERVER_USER` | SSH username for remote commands | `root` |
| `MAC_ADDRESS` | MAC address of the target server | `aa:aa:aa:aa:aa:aa` |
| `PORT` | The port number for the web server | `8080` |
The scheduled backup window configuration is stored in `schedule.json` in the installation directory. It includes the start time, end time, and frequency settings.
### Customizing Your Configuration
You can edit the `.env` file to modify the application's behavior:
```bash
# Navigate to the installation directory
cd ~/wol-server
# Edit the .env file
nano .env
```
After modifying the configuration, restart the service:
After changing configuration, restart the service:
```bash
sudo systemctl restart wol-server
```
## Usage
### Accessing the Interface
Open a web browser and navigate to:
Once installed, the WOL server will be accessible at:
```
http://your-pi-ip:8080
```
(or whatever port you've configured)
### Features
### Using the Web Interface
- **Status Checking**: The interface shows the current status (Online/Offline)
- **Booting**: Click the "Boot" button to send a WOL magic packet
- **Shutting Down**: Click "Shutdown" and enter your SSH password when prompted
- **Scheduled Backup Window**: Configure automatic server startup and shutdown on a regular schedule
1. **Wake Server**: Click the "Boot" button to send a Wake-on-LAN magic packet to the configured MAC address
2. **Shut Down Server**: Click the "Shutdown" button, confirm, and enter your password if required
#### Using Scheduled Backup Window
## Service Management
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
### Checking Service Status
Control the WOL server using standard systemd commands:
```bash
# Check service status
sudo systemctl status wol-server
# Stop the service
sudo systemctl stop wol-server
# Start the service
sudo systemctl start wol-server
# Restart the service
sudo systemctl restart wol-server
# View logs
journalctl -u wol-server -f
```
### Viewing Logs
```bash
sudo journalctl -u wol-server -f
```
### Updating
To update to a newer version:
1. Download and transfer the latest release
2. Stop the service:
```bash
sudo systemctl stop wol-server
```
3. Extract the new files:
```bash
tar -xzf wol-server.tar.gz
```
4. Run the install script:
```bash
./install.sh
```
## Troubleshooting
### Service Won't Start
### Checking Your Raspberry Pi Architecture
If you need to verify which version of the application you should use:
Check for template errors:
```bash
ls -la ~/wol-server/templates/
uname -m
```
Verify the .env file exists:
This will output your Pi's architecture:
- `armv6l`: Use the `wol-server-arm6` binary (Pi Zero, Pi 1)
- `armv7l`: Use the `wol-server-arm7` binary (Pi 2, Pi 3)
- `aarch64`: Use the `wol-server-arm64` binary (64-bit Pi 3, Pi 4)
### Service Not Starting
If the service doesn't start properly, check the logs:
```bash
cat ~/wol-server/.env
journalctl -u wol-server -e
```
### Boot Command Not Working
### Checking Configuration
1. Ensure `wakeonlan` is installed:
```bash
which wakeonlan || sudo apt-get install wakeonlan
```
2. Verify the MAC address is correct in your .env file
3. Make sure the target device is properly configured for Wake-on-LAN
Verify that your configuration is being properly loaded:
### Shutdown Not Working
1. Verify `sshpass` is installed:
```bash
which sshpass || sudo apt-get install sshpass
```
2. Check that the SERVER_USER setting in .env is correct
3. Ensure SSH access is working between your Pi and the target server
## Advanced Configuration
### Running on a Different Port
Edit the `.env` file:
```bash
echo "PORT=8181" >> ~/wol-server/.env
# View the environment variables being used
sudo systemctl status wol-server
```
### Multiple Target Machines
Look for a line in the output that shows the loaded configuration values.
### Permission Issues
Make sure the binary has execute permissions:
To control multiple devices, you can install multiple instances:
```bash
# Create a second instance
mkdir -p ~/wol-server2/templates
cp -r ~/wol-server/templates/* ~/wol-server2/templates/
cp ~/wol-server/wol-server ~/wol-server2/
# Different config
cat > ~/wol-server2/.env << EOL
SERVER_NAME=server2
SERVER_USER=admin
MAC_ADDRESS=aa:bb:cc:dd:ee:ff
PORT=8081
EOL
# Create a new service
sudo cp /etc/systemd/system/wol-server.service /etc/systemd/system/wol-server2.service
sudo sed -i 's|/home/pi/wol-server|/home/pi/wol-server2|g' /etc/systemd/system/wol-server2.service
sudo systemctl daemon-reload
sudo systemctl enable wol-server2
sudo systemctl start wol-server2
chmod +x ~/wol-server/wol-server
```
## Project Information
## Contributing
Designed for use with Raspberry Pi to provide a simple way to manage servers and devices on your local network. The web interface makes it easy to power on and off machines without having to remember MAC addresses or commands.
Contributions are welcome! Feel free to submit pull requests or open issues for bugs and feature requests.
### Contributing
Contributions are welcome! Feel free to submit pull requests or open issues to help improve this project.
### License
## License
This project is licensed under the MIT License - see the LICENSE file for details.
---
*WOL-Server - Simple Wake-on-LAN management for Raspberry Pi*

View file

@ -4,7 +4,6 @@ import (
"log"
"net/http"
"runtime"
"time"
)
// Handle the root route - show status
@ -14,11 +13,6 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Add cache control headers to prevent caching
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
online := isServerOnline()
status := "Online"
color := "#4caf50" // Material green
@ -27,9 +21,6 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
color = "#d32f2f" // Material red
}
// Get current schedule configuration
scheduleConfig := GetScheduleConfig()
data := StatusData{
Server: serverName,
Status: status,
@ -37,9 +28,6 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
IsTestMode: runtime.GOOS == "darwin",
AskPassword: false,
ErrorMessage: "",
Schedule: scheduleConfig,
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
RefreshInterval: refreshInterval,
}
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
IsTestMode: runtime.GOOS == "darwin",
AskPassword: false,
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)
@ -80,9 +65,6 @@ func bootHandler(w http.ResponseWriter, r *http.Request) {
Color: "#4caf50", // Material green
IsTestMode: runtime.GOOS == "darwin",
AskPassword: false,
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)
@ -103,9 +85,6 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) {
Color: "#d32f2f", // Material red
IsTestMode: runtime.GOOS == "darwin",
AskPassword: false,
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)
@ -114,53 +93,42 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Check if shutdown password is set
if shutdownPassword == "" {
// Show error about missing password
data := StatusData{
Server: serverName,
Status: "Online",
Color: "#4caf50", // Material green
IsTestMode: runtime.GOOS == "darwin",
AskPassword: false,
ConfirmShutdown: false,
ErrorMessage: "SHUTDOWN_PASSWORD not set in environment. Please set it in the .env file.",
Schedule: GetScheduleConfig(),
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
RefreshInterval: refreshInterval,
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
log.Printf("Template render error: %v", err)
}
return
}
// Show confirmation dialog - we'll use the password from .env
// Show confirmation dialog
data := StatusData{
Server: serverName,
Status: "Online",
Color: "#4caf50", // Material green
IsTestMode: runtime.GOOS == "darwin",
ConfirmShutdown: true,
AskPassword: false, // Make sure we don't ask for password
Schedule: GetScheduleConfig(),
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
AskPassword: false,
}
// Notify the user if password is not configured
if shutdownPassword == "" {
data.ErrorMessage = "SHUTDOWN_PASSWORD not set in environment. Shutdown may fail."
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
log.Printf("Template render error: %v", err)
}
}
// Handle shutdown confirmation without password
// enterPasswordHandler function removed - we now use the password from .env directly
// Handle password entry for shutdown
func enterPasswordHandler(w http.ResponseWriter, r *http.Request) {
if !isServerOnline() {
// Server is already offline, redirect to home
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
// Show password entry dialog
data := StatusData{
Server: serverName,
Status: "Online",
Color: "#4caf50", // Material green
IsTestMode: runtime.GOOS == "darwin",
AskPassword: true,
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
log.Printf("Template render error: %v", err)
}
}
// Handle actual shutdown request
func shutdownHandler(w http.ResponseWriter, r *http.Request) {
@ -170,20 +138,25 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Use the password from environment variable
if shutdownPassword == "" {
log.Printf("SHUTDOWN_PASSWORD not set in environment, cannot perform shutdown")
// Show error message
// Parse form data to get password
if err := r.ParseForm(); err != nil {
log.Printf("Error parsing form: %v", err)
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
// Get password from form
password := r.FormValue("password")
if password == "" {
// Show password form again with error
data := StatusData{
Server: serverName,
Status: "Online",
Color: "#4caf50",
IsTestMode: runtime.GOOS == "darwin",
AskPassword: false,
ErrorMessage: "SHUTDOWN_PASSWORD not set in environment",
Schedule: GetScheduleConfig(),
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
RefreshInterval: refreshInterval,
AskPassword: true,
ErrorMessage: "Password cannot be empty",
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
@ -193,22 +166,19 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
}
if isServerOnline() {
// Shutdown the server using the password from .env file
err := shutdownServer(shutdownPassword)
// Shutdown the server
err := shutdownServer(password)
if err != nil {
log.Printf("Error shutting down server: %v", err)
// Show error message
// Show password form again with error
data := StatusData{
Server: serverName,
Status: "Online",
Color: "#4caf50",
IsTestMode: runtime.GOOS == "darwin",
AskPassword: false, // No longer asking for password
ErrorMessage: "Failed to shutdown server. Please check the password in .env file.",
Schedule: GetScheduleConfig(),
LastUpdated: time.Now().Format("2006-01-02 15:04:05"),
RefreshInterval: refreshInterval,
AskPassword: true,
ErrorMessage: "Failed to shutdown server. Please check your password.",
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
@ -224,9 +194,6 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
Color: "#5d4037", // Material brown
IsTestMode: runtime.GOOS == "darwin",
AskPassword: false,
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)
@ -240,9 +207,6 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) {
Color: "#d32f2f", // Material red
IsTestMode: runtime.GOOS == "darwin",
AskPassword: false,
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)

391
main.go
View file

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

View file

@ -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
}

View file

@ -1,19 +1,12 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
http-equiv="refresh"
content="{{if .RefreshInterval}}{{.RefreshInterval}}{{else}}60{{end}}"
/>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server Status: {{.Server}}</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-color: {{.Color}};
@ -25,8 +18,6 @@
--success-color: #4caf50;
--modal-bg: rgba(0, 0, 0, 0.85);
--error-color: #ff6b6b;
--info-color: #2196f3;
--warning-color: #ff9800;
}
* {
@ -71,60 +62,6 @@
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 {
font-size: 48px;
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");
}
.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 {
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;
}
input[type="datetime-local"] {
color-scheme: dark;
}
.form-input:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.5);
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 {
color: var(--error-color);
font-size: 0.9rem;
@ -369,25 +266,6 @@
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 */
@media (max-width: 600px) {
.card {
@ -450,8 +328,8 @@
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
</head>
<body>
<div class="container">
<div class="card">
<div class="status-icon"></div>
@ -465,146 +343,16 @@
</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}}
<div class="test-panel">
<div class="test-note">
Running on macOS. Commands will be executed using the provided
password.
Running on macOS. Commands will be executed using the provided password.
</div>
</div>
{{end}}
<div class="footer">Wake-on-LAN Server Control Panel</div>
</div>
<!-- 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 class="footer">
Wake-on-LAN Server Control Panel
</div>
</div>
@ -613,161 +361,42 @@
<div class="modal-content">
<div class="modal-header">Confirm Shutdown</div>
<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.
</div>
<div class="modal-actions">
<a href="/" class="button">Cancel</a>
<form action="/shutdown" method="POST" style="display: inline">
<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>
<a href="/enter-password" class="button danger">Yes, Continue</a>
</div>
</div>
</div>
{{end}}
<script>
// Schedule modal handling
document.addEventListener("DOMContentLoaded", function () {
const scheduleModal = document.getElementById("scheduleModal");
const enableScheduleBtn = document.getElementById("enableSchedule");
const disableScheduleBtn = document.getElementById("disableSchedule");
const cancelScheduleBtn = document.getElementById("cancelSchedule");
const scheduleForm = document.getElementById("scheduleForm");
const scheduleError = document.getElementById("scheduleError");
{{if .AskPassword}}
<div class="modal-overlay">
<div class="modal-content">
<div class="modal-header">Enter Password</div>
<div class="modal-body">
Please enter the password for <strong>{{.Server}}</strong> to shutdown the server.
// Show schedule modal
if (enableScheduleBtn) {
enableScheduleBtn.addEventListener("click", function () {
scheduleModal.style.display = "flex";
});
}
<form action="/shutdown" method="POST">
<div class="form-group">
<label for="password" class="form-label">Password:</label>
<input type="password" id="password" name="password" class="form-input" autofocus>
</div>
// Handle auto shutdown checkbox
const autoShutdownCheckbox = document.getElementById("autoShutdown");
{{if .ErrorMessage}}
<div class="error-message">{{.ErrorMessage}}</div>
{{end}}
// Edit schedule button
const editScheduleBtn = document.getElementById("editSchedule");
if (editScheduleBtn) {
editScheduleBtn.addEventListener("click", function() {
// Pre-fill form with current values
document.getElementById("startTime").value = "{{.Schedule.StartTime}}";
document.getElementById("endTime").value = "{{.Schedule.EndTime}}";
document.getElementById("frequency").value = "{{.Schedule.Frequency}}";
document.getElementById("autoShutdown").checked = {{.Schedule.AutoShutdown}};
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>
<div class="modal-actions">
<a href="/" class="button">Cancel</a>
<button type="submit" class="button submit">Submit</button>
</div>
</form>
</div>
</div>
</div>
{{end}}
</body>
</html>

527
utils.go
View file

@ -2,7 +2,6 @@ package main
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"log"
@ -10,20 +9,8 @@ import (
"os/exec"
"path/filepath"
"runtime"
"time"
)
// ScheduleConfig holds the backup window schedule settings
type ScheduleConfig struct {
Enabled bool `json:"enabled"`
StartTime string `json:"startTime"` // Format: "HH:MM" (24-hour)
EndTime string `json:"endTime"` // Format: "HH:MM" (24-hour)
Frequency string `json:"frequency"` // "daily", "every2days", "weekly", "monthly"
LastRun string `json:"lastRun"` // ISO8601 format - when the schedule last ran
AutoShutdown bool `json:"autoShutdown"` // Whether to automatically shut down at end time
StartedBySchedule bool `json:"startedBySchedule"` // Whether server was started by scheduler
}
// StatusData holds data for the HTML template
type StatusData struct {
Server string
@ -33,15 +20,9 @@ type StatusData struct {
ConfirmShutdown bool
AskPassword bool
ErrorMessage string
Schedule ScheduleConfig
LastUpdated string
RefreshInterval int
}
var tmpl *template.Template
var scheduleConfig ScheduleConfig
var scheduleConfigPath = "schedule.json"
var shutdownPassword string // Will be loaded from environment
// Setup the HTML template
func setupTemplate() error {
@ -68,362 +49,12 @@ func setupTemplate() error {
return fmt.Errorf("failed to parse template: %v", err)
}
// Load schedule config
err = loadScheduleConfig()
if err != nil {
log.Printf("Warning: Failed to load schedule config: %v", err)
// Continue with default (empty) schedule config
}
// Check for required system tools
checkRequiredTools()
// Load shutdown password from environment
shutdownPassword = os.Getenv("SHUTDOWN_PASSWORD")
if shutdownPassword == "" {
log.Println("SHUTDOWN_PASSWORD not set in environment. Automatic shutdown will be disabled.")
} else {
log.Println("SHUTDOWN_PASSWORD loaded from environment")
}
return nil
}
// Check if required system tools are available
func checkRequiredTools() {
// Check for wakeonlan command
if _, err := exec.LookPath("wakeonlan"); err != nil {
if _, err := exec.LookPath("etherwake"); err != nil {
if _, err := exec.LookPath("wol"); err != nil {
log.Printf("WARNING: No Wake-on-LAN tools found. Please install wakeonlan, etherwake, or wol package.")
log.Printf("Installation instructions:")
log.Printf(" - For Debian/Ubuntu: sudo apt-get install wakeonlan")
log.Printf(" - For macOS: brew install wakeonlan")
log.Printf(" - For Windows: Download from https://www.depicus.com/wake-on-lan/wake-on-lan-cmd")
} else {
log.Printf("Using 'wol' for Wake-on-LAN functionality")
}
} else {
log.Printf("Using 'etherwake' for Wake-on-LAN functionality")
}
} else {
log.Printf("Found 'wakeonlan' command for Wake-on-LAN functionality")
}
// Check for ping command (needed for server status checks)
if _, err := exec.LookPath("ping"); err != nil {
log.Printf("WARNING: 'ping' command not found. Server status checks may fail.")
}
}
// Load schedule configuration from file
func loadScheduleConfig() error {
// Check if config file exists
if _, err := os.Stat(scheduleConfigPath); os.IsNotExist(err) {
// Create default config
scheduleConfig = ScheduleConfig{
Enabled: false,
StartTime: "",
EndTime: "",
Frequency: "daily",
LastRun: "",
AutoShutdown: false,
StartedBySchedule: false,
}
// Save default config
return saveScheduleConfig()
}
// Read the file
data, err := os.ReadFile(scheduleConfigPath)
if err != nil {
return fmt.Errorf("failed to read schedule config file: %v", err)
}
// Unmarshal JSON data
err = json.Unmarshal(data, &scheduleConfig)
if err != nil {
return fmt.Errorf("failed to parse schedule config: %v", err)
}
// Log loaded configuration for debugging
log.Printf("Loaded schedule config: Enabled=%v, StartTime=%s, EndTime=%s, Frequency=%s",
scheduleConfig.Enabled, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency)
return nil
}
// Save schedule configuration to file
func saveScheduleConfig() error {
data, err := json.MarshalIndent(scheduleConfig, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal schedule config: %v", err)
}
err = os.WriteFile(scheduleConfigPath, data, 0644)
if err != nil {
return fmt.Errorf("failed to save schedule config: %v", err)
}
return nil
}
// GetScheduleConfig returns the current schedule config
func GetScheduleConfig() ScheduleConfig {
return scheduleConfig
}
// UpdateScheduleConfig updates the schedule configuration
func UpdateScheduleConfig(newConfig ScheduleConfig) error {
scheduleConfig = newConfig
return saveScheduleConfig()
}
// CheckSchedule checks if server should be on/off based on schedule
func CheckSchedule() (shouldBeOn bool) {
// If schedule is not enabled, do nothing
if !scheduleConfig.Enabled {
return false
}
// If start time or end time is empty, the schedule is not properly configured
if scheduleConfig.StartTime == "" || scheduleConfig.EndTime == "" {
log.Printf("Schedule configuration incomplete: StartTime=%s, EndTime=%s",
scheduleConfig.StartTime, scheduleConfig.EndTime)
return false
}
now := time.Now()
today := now.Format("2006-01-02")
// Get current time as just hours and minutes for direct string comparison first
currentTimeStr := now.Format("15:04")
// Log the exact time comparison we're doing
log.Printf("Schedule debug: Current=%s, Start=%s, End=%s, LastRun=%s",
currentTimeStr, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.LastRun)
// Parse start time with proper error handling
startTime, err := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.StartTime))
if err != nil {
log.Printf("Error parsing start time '%s': %v", scheduleConfig.StartTime, err)
return false
}
// Parse end time with proper error handling
endTime, err := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.EndTime))
if err != nil {
log.Printf("Error parsing end time '%s': %v", scheduleConfig.EndTime, err)
return false
}
// If end time is before start time, it means the window spans to the next day
if endTime.Before(startTime) {
endTime = endTime.AddDate(0, 0, 1)
// Special case: if we're after midnight but before the end time
// we need to adjust the start time to be from yesterday
if now.Before(endTime) && now.After(time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())) {
startTime = startTime.AddDate(0, 0, -1)
}
}
// Check if the schedule should run today based on frequency
// Check if we're in the schedule window
if !ShouldRunToday(now) {
log.Printf("Schedule is active but not set to run today based on frequency: %s", scheduleConfig.Frequency)
return false
}
// Check for auto shutdown at end time
if currentTimeStr == scheduleConfig.EndTime {
if scheduleConfig.AutoShutdown && shutdownPassword != "" && isServerOnline() {
// Only shut down if the server was started by the scheduler
if scheduleConfig.StartedBySchedule {
log.Printf("Auto shutdown triggered at schedule end time %s", scheduleConfig.EndTime)
// Try up to 3 times to shut down the server
var shutdownSuccessful bool
for attempt := 1; attempt <= 3; attempt++ {
log.Printf("Auto shutdown attempt %d/3", attempt)
err := shutdownServer(shutdownPassword)
if err != nil {
log.Printf("Auto shutdown attempt %d failed: %v", attempt, err)
if attempt < 3 {
time.Sleep(3 * time.Second)
}
} else {
log.Printf("Auto shutdown initiated successfully on attempt %d", attempt)
shutdownSuccessful = true
break
}
}
if !shutdownSuccessful {
log.Printf("All auto shutdown attempts failed")
}
} else {
log.Printf("Server was not started by scheduler, skipping auto shutdown")
}
}
}
// Check if current time is within the schedule window
// Check if we're between start and end times
if currentTimeStr == scheduleConfig.StartTime {
log.Printf("Schedule match: Current time exactly matches start time")
shouldBeOn = true
} else if currentTimeStr == scheduleConfig.EndTime {
log.Printf("Schedule end: Current time exactly matches end time")
shouldBeOn = false
// Check if auto shutdown is enabled
if scheduleConfig.AutoShutdown && shutdownPassword != "" && isServerOnline() {
// Only shut down if the server was started by the scheduler
if scheduleConfig.StartedBySchedule {
log.Printf("Auto shutdown is enabled - attempting to shut down server at end time")
// Try up to 3 times to shut down the server
var shutdownSuccessful bool
for attempt := 1; attempt <= 3; attempt++ {
log.Printf("Auto shutdown attempt %d/3", attempt)
err := shutdownServer(shutdownPassword)
if err != nil {
log.Printf("Auto shutdown attempt %d failed: %v", attempt, err)
if attempt < 3 {
time.Sleep(3 * time.Second)
}
} else {
log.Printf("Auto shutdown initiated successfully on attempt %d", attempt)
shutdownSuccessful = true
break
}
}
if !shutdownSuccessful {
log.Printf("All auto shutdown attempts failed")
}
} else {
log.Printf("Server was not started by scheduler, skipping auto shutdown")
}
}
} else {
// ONLY consider the server should be on at EXACT start time or EXACT end time
shouldBeOn = (currentTimeStr == scheduleConfig.StartTime)
// Log that we're waiting for exact times for actions
if currentTimeStr != scheduleConfig.StartTime && currentTimeStr != scheduleConfig.EndTime {
log.Printf("Not at exact schedule times - no action needed until exact start/end time")
}
}
log.Printf("Schedule window check: Current=%v, Start=%v, End=%v, ShouldBeOn=%v",
now.Format("15:04:05"), startTime.Format("15:04:05"), endTime.Format("15:04:05"), shouldBeOn)
// Explicitly check end time for better debugging
if scheduleConfig.EndTime != "" && currentTimeStr == scheduleConfig.EndTime {
log.Printf("EXACT END TIME MATCH! Current time %s equals end time - schedule window should close", currentTimeStr)
}
// If we're at exact start time, update the LastRun timestamp
if currentTimeStr == scheduleConfig.StartTime && ShouldRunToday(now) {
// We only track that we've seen the start time
log.Printf("Exact start time reached - marking schedule run")
// Don't automatically boot the server here - let the main scheduler handle it
// We're just updating state information
scheduleConfig.LastRun = now.Format(time.RFC3339)
if err := saveScheduleConfig(); err != nil {
log.Printf("Warning: Failed to save schedule config: %v", err)
}
}
return shouldBeOn
}
// ShouldRunToday checks if the schedule should run today based on frequency
func ShouldRunToday(now time.Time) bool {
// We no longer check for windows - we only check at exact times
currentTimeStr := now.Format("15:04")
today := now.Format("2006-01-02")
startTime, startErr := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.StartTime))
endTime, endErr := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.EndTime))
if startErr == nil && endErr == nil {
// If end time is before start time, it means the window spans to the next day
if endTime.Before(startTime) {
endTime = endTime.AddDate(0, 0, 1)
}
// Only log that we're at an exact schedule time
if currentTimeStr == scheduleConfig.StartTime {
log.Println("Currently at exact start time - schedule should be active")
return true
}
}
// If no previous run, allow it to run
if scheduleConfig.LastRun == "" {
log.Println("No previous run recorded, schedule can run today")
return true
}
lastRun, err := time.Parse(time.RFC3339, scheduleConfig.LastRun)
if err != nil {
log.Printf("Error parsing last run date '%s': %v", scheduleConfig.LastRun, err)
// If we can't parse the date, better to let it run than to block it
return true
}
// Don't allow running multiple times on the same day unless
// it's been reset explicitly (LastRun set to empty)
if lastRun.Year() == now.Year() && lastRun.YearDay() == now.YearDay() {
// Check if we've passed the end time today - if so, we can reset for next run
if scheduleConfig.EndTime != "" && currentTimeStr > scheduleConfig.EndTime {
log.Println("Current time is after end time - resetting for next run")
scheduleConfig.LastRun = ""
scheduleConfig.StartedBySchedule = false // Reset this flag too
saveScheduleConfig()
return false
}
log.Println("Schedule already ran today, skipping")
return false
}
switch scheduleConfig.Frequency {
case "daily":
// Run every day
log.Println("Daily schedule: allowed to run today")
return true
case "every2days":
// Check if at least 2 days have passed
elapsed := now.Sub(lastRun)
eligible := elapsed >= 48*time.Hour
log.Printf("Every 2 days schedule: %v hours elapsed, eligible=%v", elapsed.Hours(), eligible)
return eligible
case "weekly":
// Check if at least 7 days have passed
elapsed := now.Sub(lastRun)
eligible := elapsed >= 7*24*time.Hour
log.Printf("Weekly schedule: %v days elapsed, eligible=%v", elapsed.Hours()/24, eligible)
return eligible
case "monthly":
// Check if last run was in a different month
sameMonth := lastRun.Month() == now.Month() && lastRun.Year() == now.Year()
log.Printf("Monthly schedule: eligible=%v", !sameMonth)
return !sameMonth
default:
log.Printf("Unknown frequency '%s', defaulting to daily", scheduleConfig.Frequency)
return true
}
}
// Check if server is online
func isServerOnline() bool {
var cmd *exec.Cmd
var stdout, stderr bytes.Buffer
// macOS and Linux have slightly different ping commands
if runtime.GOOS == "darwin" {
@ -432,184 +63,36 @@ func isServerOnline() bool {
cmd = exec.Command("ping", "-c", "1", "-W", "1", serverName)
}
// Capture output for debugging
cmd.Stdout = &stdout
cmd.Stderr = &stderr
log.Printf("Checking if server %s is online...", serverName)
err := cmd.Run()
if err != nil {
// Only log the full error in debug mode to avoid spamming the logs
if stderr.String() != "" {
log.Printf("Server %s is offline: %v - %s", serverName, err, stderr.String())
} else {
log.Printf("Server %s is offline", serverName)
}
return false
}
log.Printf("Server %s is online", serverName)
return true
return err == nil
}
// Send WOL packet
func sendWakeOnLAN() error {
log.Printf("Sending WOL packet to %s (%s)", serverName, macAddress)
// Check if wakeonlan command exists
if _, err := exec.LookPath("wakeonlan"); err == nil {
// Create the command
cmd := exec.Command("wakeonlan", macAddress)
// Capture both stdout and stderr
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
// Execute the command
err := cmd.Run()
// Log the result
if err != nil {
log.Printf("WOL command failed: %v - stderr: %s", err, stderr.String())
return fmt.Errorf("WOL command failed: %v - %s", err, stderr.String())
}
output := stdout.String()
if output != "" {
log.Printf("WOL command output: %s", output)
}
log.Printf("WOL packet sent successfully to %s", macAddress)
return nil
} else {
// wakeonlan command not found, try etherwake
if _, err := exec.LookPath("etherwake"); err == nil {
log.Printf("Using etherwake as wakeonlan alternative")
cmd := exec.Command("etherwake", macAddress)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
log.Printf("etherwake command failed: %v - stderr: %s", err, stderr.String())
return fmt.Errorf("etherwake command failed: %v - %s", err, stderr.String())
}
log.Printf("WOL packet sent successfully via etherwake to %s", macAddress)
return nil
} else {
// Try wol command as last resort
if _, err := exec.LookPath("wol"); err == nil {
log.Printf("Using wol as wakeonlan alternative")
cmd := exec.Command("wol", macAddress)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
log.Printf("wol command failed: %v - stderr: %s", err, stderr.String())
return fmt.Errorf("wol command failed: %v - %s", err, stderr.String())
}
log.Printf("WOL packet sent successfully via wol to %s", macAddress)
return nil
} else {
// Implement a fallback pure Go WOL solution
log.Printf("No WOL tools found. Please install wakeonlan, etherwake, or wol package.")
log.Printf("Installation instructions:")
log.Printf(" - For Debian/Ubuntu: sudo apt-get install wakeonlan")
log.Printf(" - For macOS: brew install wakeonlan")
log.Printf(" - For Windows: Download from https://www.depicus.com/wake-on-lan/wake-on-lan-cmd")
return fmt.Errorf("wakeonlan command not found in PATH. Please install wakeonlan tool")
}
}
}
return cmd.Run()
}
// Shutdown server with password
func shutdownServer(password string) error {
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
cmd := exec.Command("sshpass", "-p", password, "ssh",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "LogLevel=ERROR",
"-o", "ConnectTimeout=10",
fmt.Sprintf("%s@%s", serverUser, serverName),
"sudo", "-S", "shutdown", "-h", "now")
// Capture stderr to log any error messages
stderr.Reset()
var stderr bytes.Buffer
cmd.Stderr = &stderr
cmd.Stdin = bytes.NewBufferString(password + "\n")
err = cmd.Run()
if err == nil {
log.Println("SSH command executed successfully using sshpass")
return nil
}
log.Printf("sshpass method failed: %v - %s", err, stderr.String())
}
// Try direct SSH with password via stdin
log.Println("Trying direct SSH with password via stdin")
log.Printf("Password being used: %s", password)
cmd := exec.Command("ssh",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "LogLevel=ERROR",
"-o", "ConnectTimeout=10",
fmt.Sprintf("%s@%s", serverUser, serverName),
"sudo", "-S", "shutdown", "-h", "now")
stderr.Reset()
cmd.Stderr = &stderr
cmd.Stdin = bytes.NewBufferString(password + "\n")
err = cmd.Run()
if err == nil {
log.Println("SSH command executed successfully using direct SSH")
return nil
}
log.Printf("SSH Error details: %s", stderr.String())
// Try a simpler shutdown command as a fallback
log.Println("Trying simpler shutdown command as fallback")
log.Printf("Password being used: %s", password)
cmd = exec.Command("ssh",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "LogLevel=ERROR",
fmt.Sprintf("%s@%s", serverUser, serverName),
"sudo", "shutdown", "now")
stderr.Reset()
cmd.Stderr = &stderr
cmd.Stdin = bytes.NewBufferString(password + "\n")
err = cmd.Run()
err := cmd.Run()
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())
}