From 05e8bf5854c46f6009c9d05f252506091b01e173 Mon Sep 17 00:00:00 2001 From: Sebastian Mark Date: Wed, 6 Nov 2024 09:03:25 +0100 Subject: [PATCH] feat: reconnect on disconnect (add status to TUI) - add `Connected()` method for client - add `LastErr` field to GoTomatoClient - replace error logging by updating `LastErr` field - modify methods to use pointer receivers for Client - add `prevMessage` variable to store the last received server message - show connect status in TUI - use EnterAltScreen instead of ClearScreen --- cmd/client/app.go | 6 +++++- cmd/client/view.go | 15 +++++++++++---- internal/websocket/connect.go | 23 ++++++++--------------- internal/websocket/receive.go | 30 ++++++++++++++++++------------ internal/websocket/send.go | 15 +++++++++------ pkg/models/client.go | 4 +++- 6 files changed, 54 insertions(+), 39 deletions(-) diff --git a/cmd/client/app.go b/cmd/client/app.go index e29f38f..6a87b53 100644 --- a/cmd/client/app.go +++ b/cmd/client/app.go @@ -49,10 +49,14 @@ func (a app) waitForChannelSignal() tea.Cmd { func (a app) Init() tea.Cmd { client = websocket.Connect(config.URL) client.Password = config.Password + if client.LastErr != nil { + helper.Logger.Fatal(client.LastErr) + } + go client.ProcessServerMessages(a.channel) return tea.Batch( - tea.ClearScreen, + tea.EnterAltScreen, a.waitForChannelSignal(), ) } diff --git a/cmd/client/view.go b/cmd/client/view.go index eee5726..6189530 100644 --- a/cmd/client/view.go +++ b/cmd/client/view.go @@ -3,22 +3,29 @@ package client import ( "strings" + "github.com/alecthomas/colour" + "git.smsvc.net/pomodoro/ChronoTomato/cmd/client/helper" GoTomato "git.smsvc.net/pomodoro/GoTomato/pkg/models" ) func (a app) View() string { var body string + var serverStatus string + body = "Waiting for server message..." if a.pomodoro != (GoTomato.ServerMessage{}) { body = helper.TerminalOutput(a.pomodoro) helper.DesktopNotifications(a.pomodoro) - } else { - body = "Waiting for first server message..." + } + + serverStatus = colour.Sprintf("^D^1disconnected: %v^R\n", client.LastErr) + if client.Connected() { + serverStatus = colour.Sprintf("^D^2connected to %s^R\n", client.Server) } helpView := a.help.View(a.keys) - height := 8 - strings.Count(body, "\n") - strings.Count(helpView, "\n") + height := 8 - strings.Count(body, "\n") - strings.Count(serverStatus, "\n") - strings.Count(helpView, "\n") - return body + strings.Repeat("\n", height) + helpView + return body + strings.Repeat("\n", height) + serverStatus + helpView } diff --git a/internal/websocket/connect.go b/internal/websocket/connect.go index 770cd13..1f35b7b 100644 --- a/internal/websocket/connect.go +++ b/internal/websocket/connect.go @@ -4,7 +4,6 @@ import ( "github.com/gorilla/websocket" "time" - "git.smsvc.net/pomodoro/ChronoTomato/internal/helper" ChronoTomato "git.smsvc.net/pomodoro/ChronoTomato/pkg/models" ) @@ -13,30 +12,24 @@ type Client ChronoTomato.GoTomatoClient // New websocket client // Connects to websocket func Connect(url string) Client { conn, _, err := websocket.DefaultDialer.Dial(url, nil) - if err != nil { - helper.Logger.Fatal("Dial error!", "reason", err) - } - helper.Logger.Info("Connected 󰖟 ", "host", url) - time.Sleep(time.Second) + return Client{Conn: conn, Server: url, LastErr: err} +} - return Client{Conn: conn} +func (c Client) Connected() bool { + return c.Conn != nil } // Disconnects from websocket func (c Client) Disconnect() { - select { - case <-Done: - // session closed by remote - return - default: - // Cleanly close the connection by sending a close message and then - // waiting (with timeout) for the server to close the connection. + // Cleanly close the connection by sending a close message and then + // waiting (with timeout) for the server to close the connection. + if c.Connected() { c.Conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) select { case <-Done: case <-time.After(time.Second): } - return } + return } diff --git a/internal/websocket/receive.go b/internal/websocket/receive.go index bb36722..4aeb6ad 100644 --- a/internal/websocket/receive.go +++ b/internal/websocket/receive.go @@ -2,9 +2,10 @@ package websocket import ( "encoding/json" + "time" + "github.com/gorilla/websocket" - "git.smsvc.net/pomodoro/ChronoTomato/internal/helper" GoTomato "git.smsvc.net/pomodoro/GoTomato/pkg/models" ) @@ -12,30 +13,35 @@ var Done = make(chan struct{}) // Receives websocket messages and writes them to a channel. // Closes the channel if websocket closes. -func (c Client) ProcessServerMessages(channel chan<- GoTomato.ServerMessage) { - var serverMessage GoTomato.ServerMessage +func (c *Client) ProcessServerMessages(channel chan<- GoTomato.ServerMessage) { + var serverMessage, prevMessage GoTomato.ServerMessage defer close(Done) for { _, message, err := c.Conn.ReadMessage() if err != nil { + // On normal closure exit gracefully if websocket.IsCloseError(err, websocket.CloseNormalClosure) { - // Ignore normal closure and exit gracefully return } - // Log any other errors - helper.Logger.Error("Read error!", "reason", err) - close(channel) - return - } - err = json.Unmarshal(message, &serverMessage) - if err != nil { - helper.Logger.Error("Error unmarshalling!", "reason", err) + // Try to reconnect on unexpected disconnect + for { + channel <- prevMessage // send previous ServerMessage to update view + time.Sleep(time.Second) + *c = Connect(c.Server) + if c.Connected() { + break + } + } continue } + err = json.Unmarshal(message, &serverMessage) + c.LastErr = err + channel <- serverMessage + prevMessage = serverMessage } } diff --git a/internal/websocket/send.go b/internal/websocket/send.go index a3126cc..5bebbd6 100644 --- a/internal/websocket/send.go +++ b/internal/websocket/send.go @@ -4,27 +4,30 @@ import ( "encoding/json" "github.com/gorilla/websocket" - "git.smsvc.net/pomodoro/ChronoTomato/internal/helper" GoTomato "git.smsvc.net/pomodoro/GoTomato/pkg/models" ) // Sends a message to the clients websocket -func sendClientCommand(c Client, msg GoTomato.ClientCommand) { +func sendClientCommand(c *Client, msg GoTomato.ClientCommand) { + if !c.Connected() { + // noop if client is not connected + return + } messageBytes, err := json.Marshal(msg) if err != nil { - helper.Logger.Error("Error marshalling!", "reason", err) + c.LastErr = err return } err = c.Conn.WriteMessage(websocket.TextMessage, messageBytes) if err != nil { - helper.Logger.Error("Write error!", "reason", err) + c.LastErr = err return } } // Sends the passed command to the server -func (c Client) SendCmd(cmd string) { +func (c *Client) SendCmd(cmd string) { message := GoTomato.ClientCommand{ Command: cmd, Password: c.Password, @@ -34,7 +37,7 @@ func (c Client) SendCmd(cmd string) { } // Sends the new PomodoroConfig to the server -func (c Client) SendSettingsUpdate(settings GoTomato.PomodoroConfig) { +func (c *Client) SendSettingsUpdate(settings GoTomato.PomodoroConfig) { message := GoTomato.ClientCommand{ Command: "updateSettings", Password: c.Password, diff --git a/pkg/models/client.go b/pkg/models/client.go index f2bb4c7..5740c05 100644 --- a/pkg/models/client.go +++ b/pkg/models/client.go @@ -4,6 +4,8 @@ import "github.com/gorilla/websocket" // Represents a GoTomato client type GoTomatoClient struct { - Conn *websocket.Conn // The websocket connection of the client + Server string // The GoTomato server Password string // Pomodoro password + Conn *websocket.Conn // The websocket connection of the client + LastErr error // Last client error }