Skip to content

Commit

Permalink
feat: support pinging machines to get current status
Browse files Browse the repository at this point in the history
  • Loading branch information
Trugamr committed Jan 20, 2025
1 parent 24823d9 commit a9b8cb3
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 22 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A CLI tool to send Wake-On-LAN (WOL) magic packets to wake up devices on your
network. Features both CLI commands and a web interface.

<img src="assets/images/web.png" alt="Web Interface" width="720">
<img src="assets/images/web.png" alt="Web Interface" />

## Features

Expand Down Expand Up @@ -46,8 +46,10 @@ Example configuration:
machines:
- name: desktop
mac: "00:11:22:33:44:55"
ip: "192.168.1.100" # Optional, for status checking
- name: server
mac: "AA:BB:CC:DD:EE:FF"
ip: "192.168.1.101" # Optional, for status checking

server:
listen: ":7777" # Optional, defaults to :7777
Expand Down Expand Up @@ -81,6 +83,7 @@ command. It provides:

- List of all configured machines
- One-click wake up buttons
- Real-time machine status monitoring (when IP is configured)
- Version information
- Links to documentation and support

Expand Down
Binary file modified assets/images/web.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
172 changes: 164 additions & 8 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ package cmd

import (
"embed"
"encoding/json"
"fmt"
"html/template"
"log"
"net"
"net/http"
"runtime"
"sync"
"time"

probing "github.com/prometheus-community/pro-bing"
"github.com/spf13/cobra"
"github.com/trugamr/wol/config"
)

//go:embed templates/*
Expand All @@ -26,6 +34,7 @@ var serveCmd = &cobra.Command{

mux.HandleFunc("GET /{$}", handleIndex)
mux.HandleFunc("POST /wake", handleWake)
mux.HandleFunc("GET /status", handleStatus)

log.Printf("Listening on %s", cfg.Server.Listen)
err := http.ListenAndServe(cfg.Server.Listen, mux)
Expand All @@ -46,10 +55,11 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {

// Execute the template
data := map[string]interface{}{
"Machines": cfg.Machines,
"Version": version,
"Commit": commit,
"Date": date,
"Machines": cfg.Machines,
"Version": version,
"Commit": commit,
"Date": date,
"FlashMessage": consumeFlashMessage(w, r), // Get flash message from cookie
}
err = index.Execute(w, data)
if err != nil {
Expand All @@ -59,11 +69,35 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
}
}

func handleWake(w http.ResponseWriter, r *http.Request) {
// If we were to get MAC address, validate if it exists in the config
// If it does, send the magic packet
// setFlashMessage sets a flash message in a cookie
func setFlashMessage(w http.ResponseWriter, message string) {
http.SetCookie(w, &http.Cookie{
Name: "flash",
Value: message,
Path: "/",
})
}

mac, err := getMacByName(r.FormValue("name"))
// consumeFlashMessage retrieves and clears the flash message from the request
func consumeFlashMessage(w http.ResponseWriter, r *http.Request) string {
cookie, err := r.Cookie("flash")
if err == nil {
// Clear the cookie
http.SetCookie(w, &http.Cookie{
Name: "flash",
Value: "",
Path: "/",
Expires: time.Now().Add(-1 * time.Hour),
})

return cookie.Value
}
return ""
}

func handleWake(w http.ResponseWriter, r *http.Request) {
machineName := r.FormValue("name")
mac, err := getMacByName(machineName)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
Expand All @@ -77,5 +111,127 @@ func handleWake(w http.ResponseWriter, r *http.Request) {
return
}

// Set flash message cookie
setFlashMessage(w, fmt.Sprintf("Wake-up signal sent to %s. The machine should wake up shortly.", machineName))

http.Redirect(w, r, "/", http.StatusSeeOther)
}

// getMachineStatus returns the status of a machine
func getMachineStatus(machine config.Machine) (string, error) {
if machine.IP == nil {
return "unknown", nil
}
ip := net.ParseIP(*machine.IP)
if ip == nil {
return "unknown", fmt.Errorf("invalid IP address: %s", *machine.IP)
}

// if !isAddressReachable(ip) {
// return "offline", nil
// }
reachable, err := isAddressReachable(ip)
if err != nil {
return "unknown", err
}
if reachable {
return "online", nil
}

return "offline", nil
}

// getMachinesStatus returns a map of machine names to their statuses concurrently
func getMachinesStatus() map[string]string {
var mu sync.Mutex
statuses := make(map[string]string)
var wg sync.WaitGroup

for _, machine := range cfg.Machines {
wg.Add(1)
go func(machine config.Machine) {
defer wg.Done()
status, err := getMachineStatus(machine)
if err != nil {
log.Printf("Error getting status for machine %s: %v", machine.Name, err)
return
}

mu.Lock()
statuses[machine.Name] = status
mu.Unlock()
}(machine)
}

wg.Wait()

return statuses
}

func handleStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")

// Sends the current status of all machines
sendMachinesStatus := func() {
statuses := getMachinesStatus()
data, err := json.Marshal(statuses)
if err != nil {
log.Printf("Error marshaling status: %v", err)
return
}

_, err = fmt.Fprintf(w, "data: %s\n\n", data)
if err != nil {
log.Printf("Error writing status: %v", err)
return
}

w.(http.Flusher).Flush()
}

// Sends initial status
sendMachinesStatus()

// Send status updates every few seconds
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()

for {
select {
case <-r.Context().Done():
return
case <-ticker.C:
sendMachinesStatus()
}
}
}

func isAddressReachable(ip net.IP) (bool, error) {
pinger, err := probing.NewPinger(ip.String())
if err != nil {
return false, fmt.Errorf("error creating pinger: %v", err)
}
// /~https://github.com/prometheus-community/pro-bing?tab=readme-ov-file#windows
if runtime.GOOS == "windows" {
pinger.SetPrivileged(true)
}

// We only want to ping once and wait 2 seconds for a response
pinger.Timeout = 2 * time.Second
pinger.Count = 1

err = pinger.Run()
if err != nil {
return false, fmt.Errorf("error pinging: %v", err)
}

// If we receive even a single packet, the address is reachable
stats := pinger.Statistics()
if stats.PacketsRecv == 0 {
return false, nil
}

return true, nil
}
Loading

0 comments on commit a9b8cb3

Please sign in to comment.