mirror of
https://github.com/Kioubit/ColorPing
synced 2024-12-03 21:20:40 +08:00
212 lines
4 KiB
Go
212 lines
4 KiB
Go
package main
|
|
|
|
import (
|
|
"embed"
|
|
"fmt"
|
|
"html/template"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
//go:embed template.html
|
|
var embedFS embed.FS
|
|
var htmlTemplate *template.Template
|
|
|
|
type clientState int
|
|
|
|
const (
|
|
INITIAL = iota
|
|
ACTIVE
|
|
)
|
|
|
|
type client struct {
|
|
channel chan string
|
|
state clientState
|
|
}
|
|
|
|
var (
|
|
clients = make([]*client, 0)
|
|
clientMutex sync.Mutex
|
|
)
|
|
|
|
func deleteClient(client *client) {
|
|
for i := 0; i < len(clients); i++ {
|
|
if clients[i] == client {
|
|
clients[i] = clients[len(clients)-1]
|
|
clients = clients[:len(clients)-1]
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func httpServer() error {
|
|
htmlTemplate = template.Must(template.ParseFS(embedFS, "template.html"))
|
|
http.HandleFunc("/stream", stream)
|
|
http.HandleFunc("/", serveRoot)
|
|
return http.ListenAndServe(":9090", nil)
|
|
}
|
|
|
|
func getInterfaceBaseIP() string {
|
|
iFace, err := net.InterfaceByName(interfaceName)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
addresses, err := iFace.Addrs()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
gua := ""
|
|
ula := ""
|
|
for _, v := range addresses {
|
|
addr := v.String()
|
|
if !strings.Contains(addr, ":") {
|
|
continue
|
|
}
|
|
_, anet, err := net.ParseCIDR(addr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if anet.IP.IsLinkLocalUnicast() {
|
|
continue
|
|
}
|
|
if anet.IP.IsGlobalUnicast() {
|
|
gua = strings.Split(anet.String(), "/")[0]
|
|
}
|
|
if anet.IP.IsPrivate() {
|
|
ula = strings.Split(anet.String(), "/")[0]
|
|
}
|
|
}
|
|
if gua != "" {
|
|
return gua
|
|
} else {
|
|
return ula
|
|
}
|
|
}
|
|
|
|
func serveRoot(w http.ResponseWriter, r *http.Request) {
|
|
if r.RequestURI != "/" {
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
type pageData struct {
|
|
BaseIP string
|
|
CanvasWidth int
|
|
CanvasHeight int
|
|
}
|
|
baseIP := getInterfaceBaseIP()
|
|
if len(baseIP) == 21 {
|
|
baseIP = strings.TrimSuffix(baseIP, ":")
|
|
}
|
|
err := htmlTemplate.Execute(w, pageData{
|
|
BaseIP: baseIP,
|
|
CanvasHeight: 512,
|
|
CanvasWidth: 512,
|
|
})
|
|
if err != nil {
|
|
fmt.Println("Error executing HTML template:", err)
|
|
}
|
|
}
|
|
|
|
var streamServerRunning atomic.Bool
|
|
|
|
func streamServer() {
|
|
if !streamServerRunning.CompareAndSwap(false, true) {
|
|
return
|
|
}
|
|
go func() {
|
|
for {
|
|
clientMutex.Lock()
|
|
if len(clients) == 0 {
|
|
streamServerRunning.Store(false)
|
|
clientMutex.Unlock()
|
|
return
|
|
}
|
|
|
|
requiresInitial := false
|
|
requiresUpdate := false
|
|
for _, v := range clients {
|
|
if v.state == INITIAL {
|
|
requiresInitial = true
|
|
} else {
|
|
requiresUpdate = true
|
|
}
|
|
if requiresInitial && requiresUpdate {
|
|
break
|
|
}
|
|
}
|
|
|
|
dataInitial, dataUpdate := getPicture(requiresInitial, requiresUpdate)
|
|
tmp := clients[:0]
|
|
for _, v := range clients {
|
|
if v.state == INITIAL {
|
|
v.state = ACTIVE
|
|
select {
|
|
case v.channel <- dataInitial:
|
|
default:
|
|
}
|
|
} else {
|
|
if dataUpdate != "0" {
|
|
select {
|
|
case v.channel <- dataUpdate:
|
|
default:
|
|
// Client cannot keep up
|
|
close(v.channel)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
tmp = append(tmp, v)
|
|
}
|
|
clients = tmp
|
|
clientMutex.Unlock()
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func stream(w http.ResponseWriter, r *http.Request) {
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
messageChan := make(chan string, 40)
|
|
newClient := &client{
|
|
channel: messageChan,
|
|
state: INITIAL,
|
|
}
|
|
clientMutex.Lock()
|
|
clients = append(clients, newClient)
|
|
clientMutex.Unlock()
|
|
streamServer()
|
|
|
|
// For when clients are removed prior to connection close, to avoid a call to deleteClient()
|
|
var channelClosedFirst = false
|
|
go func() {
|
|
// Listen for connection close
|
|
<-r.Context().Done()
|
|
clientMutex.Lock()
|
|
if !channelClosedFirst {
|
|
deleteClient(newClient)
|
|
}
|
|
close(messageChan)
|
|
clientMutex.Unlock()
|
|
}()
|
|
|
|
for {
|
|
data, ok := <-messageChan
|
|
if !ok {
|
|
channelClosedFirst = true
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(data))
|
|
flusher.Flush()
|
|
}
|
|
}
|