From cb150681092c5b59433979fa2ce49934ba24857b Mon Sep 17 00:00:00 2001 From: Sebastian Mark Date: Sat, 19 Oct 2024 11:47:56 +0200 Subject: [PATCH] feat(server): restructure Pomodoro server into modular components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - move server logic to cmd/server/main.go - create packages for websocket, pomodoro and broadcast handling - define models for messages - remove old GoTomato.go file - update README 🤖 --- GoTomato.go | 155 ----------------------- README.md | 2 +- cmd/server/main.go | 17 +++ go.mod | 2 +- internal/broadcast/broadcast.go | 28 ++++ internal/pomodoro/pomodoro.go | 39 ++++++ internal/pomodoro/timer.go | 51 ++++++++ internal/websocket/client_commands.go | 41 ++++++ internal/websocket/handle_connections.go | 32 +++++ pkg/models/types.go | 14 ++ 10 files changed, 224 insertions(+), 157 deletions(-) delete mode 100644 GoTomato.go create mode 100644 cmd/server/main.go create mode 100644 internal/broadcast/broadcast.go create mode 100644 internal/pomodoro/pomodoro.go create mode 100644 internal/pomodoro/timer.go create mode 100644 internal/websocket/client_commands.go create mode 100644 internal/websocket/handle_connections.go create mode 100644 pkg/models/types.go diff --git a/GoTomato.go b/GoTomato.go deleted file mode 100644 index 28a0917..0000000 --- a/GoTomato.go +++ /dev/null @@ -1,155 +0,0 @@ -package main - -import ( - "encoding/json" - "log" - "net/http" - "sync" - "time" - - "github.com/gorilla/websocket" -) - -const ( - workDuration = 15 * 60 - shortBreakDuration = 5 * 60 - longBreakDuration = 10 * 60 - sessions = 4 -) - -type BroadcastMessage struct { - Mode string `json:"mode"` - Session int `json:"session"` - MaxSession int `json:"max_session"` - TimeLeft int `json:"time_left"` -} - -type ClientCommand struct { - Command string `json:"command"` -} - -var clients = make(map[*websocket.Conn]bool) -var timerRunning bool -var timerStopChannel = make(chan bool, 1) -var mu sync.Mutex // to synchronize access to shared state - -// broadcastMessage sends the remaining time to all connected clients. -func broadcastMessage(message BroadcastMessage) { - jsonMessage, _ := json.Marshal(message) - for client := range clients { - err := client.WriteMessage(websocket.TextMessage, jsonMessage) - if err != nil { - log.Printf("Error broadcasting to client: %v", err) - client.Close() - delete(clients, client) - } - } -} - -// startTimer runs the countdown and broadcasts every second. -func startTimer(remainingSeconds int, mode string, session int) bool { - for remainingSeconds > 0 { - select { - case <-timerStopChannel: - return false // Stop the timer if a stop command is received - default: - broadcastMessage(BroadcastMessage{ - Mode: mode, - Session: session, - MaxSession: sessions, - TimeLeft: remainingSeconds, - }) - time.Sleep(time.Second) - remainingSeconds-- - } - } - broadcastMessage(BroadcastMessage{ - Mode: mode, - Session: session, - MaxSession: sessions, - TimeLeft: 0, - }) - return true -} - -// runPomodoroTimer iterates the Pomodoro work/break sessions. -func runPomodoroTimer() { - mu.Lock() - timerRunning = true - - for session := 1; session <= sessions; session++ { - if !startTimer(workDuration, "Work", session) { - break - } - if session == sessions { - if !startTimer(longBreakDuration, "LongBreak", session) { - break - } - } else { - if !startTimer(shortBreakDuration, "ShortBreak", session) { - break - } - } - } - - timerRunning = false - mu.Unlock() -} - -// upgrade HTTP requests to WebSocket connections. -var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, -} - -func handleConnections(w http.ResponseWriter, r *http.Request) { - // Upgrade initial GET request to a WebSocket - ws, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Printf("WebSocket upgrade error: %v", err) - return - } - defer ws.Close() - - // Register the new client - clients[ws] = true - - // Listen for commands from this client - for { - _, message, err := ws.ReadMessage() - if err != nil { - log.Printf("Client disconnected: %v", err) - delete(clients, ws) - break - } - - // Handle incoming commands - var command ClientCommand - err = json.Unmarshal(message, &command) - if err != nil { - log.Printf("Error unmarshalling command: %v", err) - continue - } - - // Process the commands - switch command.Command { - case "start": - if !timerRunning { - go runPomodoroTimer() - } - case "stop": - if timerRunning { - timerStopChannel <- true - } - } - } -} - -func main() { - http.HandleFunc("/ws", handleConnections) - - log.Println("Pomodoro WebSocket server started on :8080") - err := http.ListenAndServe(":8080", nil) - if err != nil { - log.Fatalf("Error starting server: %v", err) - } -} diff --git a/README.md b/README.md index 2769187..35d94c8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A pomodoro server written in Go ``` docker run --rm -d --name pomodoro-client -v $PWD:/usr/share/nginx/html/ -p 8081:80 nginx -go run . +go run ./cmd/server ``` open http://localhost:8081 diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..bf89583 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "git.smsvc.net/pomodoro/GoTomato/internal/websocket" + "log" + "net/http" +) + +func main() { + http.HandleFunc("/ws", websocket.HandleConnections) + + log.Println("Pomodoro WebSocket server started on :8080") + err := http.ListenAndServe(":8080", nil) + if err != nil { + log.Fatalf("Error starting server: %v", err) + } +} diff --git a/go.mod b/go.mod index b487ee5..c53e4ce 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module git.smsvc.net/pomodoro/GoTomato go 1.23.1 -require github.com/gorilla/websocket v1.5.3 // indirect +require github.com/gorilla/websocket v1.5.3 diff --git a/internal/broadcast/broadcast.go b/internal/broadcast/broadcast.go new file mode 100644 index 0000000..deb3010 --- /dev/null +++ b/internal/broadcast/broadcast.go @@ -0,0 +1,28 @@ +package broadcast + +import ( + "encoding/json" + "git.smsvc.net/pomodoro/GoTomato/pkg/models" + "github.com/gorilla/websocket" + "log" +) + +// BroadcastMessage sends a message to all connected WebSocket clients. +func BroadcastMessage(clients map[*websocket.Conn]bool, message models.BroadcastMessage) { + // Marshal the message into JSON format + jsonMessage, err := json.Marshal(message) + if err != nil { + log.Printf("Error marshalling message: %v", err) + return + } + + // Iterate over all connected clients and broadcast the message + for client := range clients { + err := client.WriteMessage(websocket.TextMessage, jsonMessage) + if err != nil { + log.Printf("Error broadcasting to client: %v", err) + client.Close() + delete(clients, client) // Remove the client if an error occurs + } + } +} diff --git a/internal/pomodoro/pomodoro.go b/internal/pomodoro/pomodoro.go new file mode 100644 index 0000000..02325ac --- /dev/null +++ b/internal/pomodoro/pomodoro.go @@ -0,0 +1,39 @@ +package pomodoro + +import ( + "github.com/gorilla/websocket" + "sync" +) + +const ( + workDuration = 15 * 60 + shortBreakDuration = 5 * 60 + longBreakDuration = 10 * 60 + sessions = 4 +) + +var mu sync.Mutex // to synchronize access to shared state + +// RunPomodoroTimer iterates the Pomodoro work/break sessions. +func RunPomodoroTimer(clients map[*websocket.Conn]bool) { + mu.Lock() + timerRunning = true + + for session := 1; session <= sessions; session++ { + if !startTimer(clients, workDuration, "Work", session) { + break + } + if session == sessions { + if !startTimer(clients, longBreakDuration, "LongBreak", session) { + break + } + } else { + if !startTimer(clients, shortBreakDuration, "ShortBreak", session) { + break + } + } + } + + timerRunning = false + mu.Unlock() +} diff --git a/internal/pomodoro/timer.go b/internal/pomodoro/timer.go new file mode 100644 index 0000000..13364a1 --- /dev/null +++ b/internal/pomodoro/timer.go @@ -0,0 +1,51 @@ +package pomodoro + +import ( + "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" + "git.smsvc.net/pomodoro/GoTomato/pkg/models" + "github.com/gorilla/websocket" + "time" +) + +var timerRunning bool +var timerStopChannel = make(chan bool, 1) + +// startTimer runs the countdown and broadcasts every second. +func startTimer(clients map[*websocket.Conn]bool, remainingSeconds int, mode string, session int) bool { + for remainingSeconds > 0 { + select { + case <-timerStopChannel: + return false // Stop the timer if a stop command is received + default: + // Broadcast the current state to all clients + broadcast.BroadcastMessage(clients, models.BroadcastMessage{ + Mode: mode, + Session: session, + MaxSession: sessions, + TimeLeft: remainingSeconds, + }) + time.Sleep(time.Second) + remainingSeconds-- + } + } + + // Final broadcast when time reaches zero + broadcast.BroadcastMessage(clients, models.BroadcastMessage{ + Mode: mode, + Session: session, + MaxSession: sessions, + TimeLeft: 0, + }) + + return true +} + +// StopTimer sends a signal to stop the running Pomodoro timer. +func StopTimer() { + timerStopChannel <- true +} + +// IsTimerRunning returns the status of the timer. +func IsTimerRunning() bool { + return timerRunning +} diff --git a/internal/websocket/client_commands.go b/internal/websocket/client_commands.go new file mode 100644 index 0000000..fe79513 --- /dev/null +++ b/internal/websocket/client_commands.go @@ -0,0 +1,41 @@ +package websocket + +import ( + "encoding/json" + "git.smsvc.net/pomodoro/GoTomato/internal/pomodoro" + "git.smsvc.net/pomodoro/GoTomato/pkg/models" + "github.com/gorilla/websocket" + "log" +) + +// handleClientCommands listens for commands from WebSocket clients and dispatches to the timer. +func handleClientCommands(ws *websocket.Conn) { + for { + _, message, err := ws.ReadMessage() + if err != nil { + log.Printf("Client disconnected: %v", err) + delete(Clients, ws) + break + } + + // Handle incoming commands + var command models.ClientCommand + err = json.Unmarshal(message, &command) + if err != nil { + log.Printf("Error unmarshalling command: %v", err) + continue + } + + // Process the command + switch command.Command { + case "start": + if !pomodoro.IsTimerRunning() { + go pomodoro.RunPomodoroTimer(Clients) // Start the timer with the list of clients + } + case "stop": + if pomodoro.IsTimerRunning() { + pomodoro.StopTimer() // Stop the timer in the Pomodoro package + } + } + } +} diff --git a/internal/websocket/handle_connections.go b/internal/websocket/handle_connections.go new file mode 100644 index 0000000..a7d6cfc --- /dev/null +++ b/internal/websocket/handle_connections.go @@ -0,0 +1,32 @@ +package websocket + +import ( + "github.com/gorilla/websocket" + "log" + "net/http" +) + +// Map to track connected clients +var Clients = make(map[*websocket.Conn]bool) + +// Upgrader to upgrade HTTP requests to WebSocket connections +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, +} + +// HandleConnections upgrades HTTP requests to WebSocket connections and manages the client lifecycle. +func HandleConnections(w http.ResponseWriter, r *http.Request) { + // Upgrade initial GET request to a WebSocket + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("WebSocket upgrade error: %v", err) + return + } + defer ws.Close() + + // Register the new client + Clients[ws] = true + + // Listen for commands from the connected client + handleClientCommands(ws) +} diff --git a/pkg/models/types.go b/pkg/models/types.go new file mode 100644 index 0000000..ea04e21 --- /dev/null +++ b/pkg/models/types.go @@ -0,0 +1,14 @@ +package models + +// BroadcastMessage represents the data sent to the client via WebSocket. +type BroadcastMessage struct { + Mode string `json:"mode"` // "Work", "ShortBreak", or "LongBreak" + Session int `json:"session"` // Current session number + MaxSession int `json:"max_session"` // Total number of sessions + TimeLeft int `json:"time_left"` // Remaining time in seconds +} + +// ClientCommand represents a command from the client (start/stop). +type ClientCommand struct { + Command string `json:"command"` +}