Skip to content

Commit

Permalink
feat: persist irc connections between websocket disconnects (#164)
Browse files Browse the repository at this point in the history
- currently, when the websocket connection is dropped, the irc
  connection is closed immediately. this change makes it so that
  after the websocket connection closed, the user has 3 minutes
  to reconnect before we close the irc connection.
- the 3 minute 'self-destruct' is only triggered when the websocket
  is disconnected and is disabled once there is another connection
- this should help with certain devices (such as iPad) where switching
  apps / tabs seems to close the websocket. i believe that the IRC
  server is sensitive to clients connecting/disconnecting too often and
  will ban users.
  • Loading branch information
evan-buss authored May 5, 2024
1 parent c2c8e01 commit bd1859b
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 29 deletions.
12 changes: 10 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,19 @@
"remoteUser": "root",
"features": {
"ghcr.io/devcontainers/features/go:1": {},
"ghcr.io/devcontainers/features/node:1": {}
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"customizations": {
"vscode": {
"extensions": ["golang.go", "heybourn.headwind", "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss", "ms-vscode.makefile-tools", "redhat.vscode-yaml"]
"extensions": [
"golang.go",
"heybourn.headwind",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"ms-vscode.makefile-tools",
"redhat.vscode-yaml"
]
}
}
}
1 change: 0 additions & 1 deletion server/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ type Client struct {
// reads from this goroutine.
func (server *server) readPump(c *Client) {
defer func() {
c.irc.Disconnect()
c.conn.Close()
server.unregister <- c
}()
Expand Down
6 changes: 2 additions & 4 deletions server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/evan-buss/openbooks/irc"
"io/fs"
"log"
"net/http"
Expand All @@ -17,6 +16,8 @@ import (
"strings"
"time"

"github.com/evan-buss/openbooks/irc"

"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
Expand Down Expand Up @@ -90,9 +91,6 @@ func (server *server) serveWs() http.HandlerFunc {
client.log.Println("New client created.")

server.register <- client

go server.writePump(client)
go server.readPump(client)
}
}

Expand Down
42 changes: 36 additions & 6 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,47 @@ func Start(config Config) {
// The client hub is to be run in a goroutine and handles management of
// websocket client registrations.
func (server *server) startClientHub(ctx context.Context) {
type selfDestructor struct {
timer *time.Timer
client *Client
}

selfDestructors := make(map[uuid.UUID]selfDestructor)

for {
select {
case client := <-server.register:
if destructor, ok := selfDestructors[client.uuid]; ok {
destructor.timer.Stop()
server.log.Printf("Client %s reconnected\n", client.uuid.String())

// Update the existing client's websocket connection to the new one
destructor.client.conn = client.conn
client = destructor.client

delete(selfDestructors, client.uuid)
}

server.clients[client.uuid] = client
go server.writePump(client)
go server.readPump(client)

case client := <-server.unregister:
if _, ok := server.clients[client.uuid]; ok {
_, cancel := context.WithCancel(client.ctx)
close(client.send)
cancel()
delete(server.clients, client.uuid)
}
// Keep the client and IRC connection alive for 3 minutes in case the client reconnects
timer := time.AfterFunc(time.Minute*3, func() {
if _, ok := selfDestructors[client.uuid]; ok {
client.irc.Disconnect()
_, cancel := context.WithCancel(client.ctx)
close(client.send)
cancel()
delete(selfDestructors, client.uuid)
server.log.Printf("Client %s self-destructed\n", client.uuid.String())
}
})

selfDestructors[client.uuid] = selfDestructor{timer, client}
delete(server.clients, client.uuid)
server.log.Printf("Client %s disconnected\n", client.uuid.String())
case <-ctx.Done():
for _, client := range server.clients {
_, cancel := context.WithCancel(client.ctx)
Expand Down
48 changes: 32 additions & 16 deletions server/websocket_requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,34 +49,50 @@ func (server *server) routeMessage(message Request, c *Client) {

// handle ConnectionRequests and either connect to the server or do nothing
func (c *Client) startIrcConnection(server *server) {
err := core.Join(c.irc, server.config.Server, server.config.EnableTLS)
if err != nil {
c.log.Println(err)
c.send <- newErrorResponse("Unable to connect to IRC server.")
return
}
// The IRC connection could be re-used if it is already connected
if !c.irc.IsConnected() {
err := core.Join(c.irc, server.config.Server, server.config.EnableTLS)
if err != nil {
c.log.Println(err)
c.send <- newErrorResponse("Unable to connect to IRC server.")
return
}

handler := server.NewIrcEventHandler(c)
handler := server.NewIrcEventHandler(c)

if server.config.Log {
logger, _, err := util.CreateLogFile(c.irc.Username, server.config.DownloadDir)
if err != nil {
server.log.Println(err)
if server.config.Log {
logger, _, err := util.CreateLogFile(c.irc.Username, server.config.DownloadDir)
if err != nil {
server.log.Println(err)
}
handler[core.Message] = func(text string) { logger.Println(text) }
}
handler[core.Message] = func(text string) { logger.Println(text) }
}

go core.StartReader(c.ctx, c.irc, handler)
go core.StartReader(c.ctx, c.irc, handler)

c.send <- ConnectionResponse{
StatusResponse: StatusResponse{
MessageType: CONNECT,
NotificationType: SUCCESS,
Title: "Welcome, connection established.",
Detail: fmt.Sprintf("IRC username %s", c.irc.Username),
},
Name: c.irc.Username,
}

return
}

c.send <- ConnectionResponse{
StatusResponse: StatusResponse{
MessageType: CONNECT,
NotificationType: SUCCESS,
Title: "Welcome, connection established.",
NotificationType: NOTIFY,
Title: "Welcome back, re-using open IRC connection.",
Detail: fmt.Sprintf("IRC username %s", c.irc.Username),
},
Name: c.irc.Username,
}

}

// handle SearchRequests and send the query to the book server
Expand Down

0 comments on commit bd1859b

Please sign in to comment.