package websocket
import (
"encoding/json"
"fmt"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"io"
"time"
)
type writer struct {
// conn is the websocket connection that this writer is responsible for writing on.
conn * websocket . Conn
// channel is the channel used to receive server messages to be sent to the client.
// When it receives a SocketClosed, the process on the sending end promises not to send any further messages, as the writer will close it right after.
channel chan ServerCommand
// readNotifications is the channel used to receive pings when the reader receives a message, so that a ping will be sent out before the reader is ready to time out.
// When it is closed, the reader has shut down.
readNotifications <- chan time . Time
// timer is the timer used to send pings when the reader is close to timing out, to make sure the other end of the connection is still listening.
timer * time . Timer
// nextPingAt is the time after which the next ping will be sent if the timer is ticking. It will be .IsZero() if the timer is not ticking.
nextPingAt time . Time
// logger is the logger used to record the state of the writer, primarily in Debug level.
logger * zap . Logger
}
// IsTimerTicking returns true if the writer's timer is running.
func ( w * writer ) isTimerTicking ( ) bool {
return ! w . nextPingAt . IsZero ( )
}
// sendNextPingAt starts the timer if it's not running, and
func ( w * writer ) sendNextPingAt ( nextPing time . Time ) {
if w . nextPingAt . IsZero ( ) {
// Timer is not running, so set the next ping time and start it.
w . nextPingAt = nextPing
w . timer . Reset ( time . Until ( nextPing ) )
} else if w . nextPingAt . Before ( nextPing ) {
// Timer's already running, so leave it be, but update the next ping time.
w . nextPingAt = nextPing
} else {
// The timer is already set to a time after the incoming time.
// It's extremely unlikely for this empty branch to ever be reached.
}
}
// act is the function responsible for actually doing the writing.
func ( w * writer ) act ( ) {
defer w . gracefulShutdown ( )
w . logger . Debug ( "Starting up" )
w . timer = time . NewTimer ( PingDelay )
w . nextPingAt = time . Now ( ) . Add ( PingDelay )
for {
select {
case readAt , open := <- w . readNotifications :
if open {
nextPingAt := readAt . Add ( PingDelay )
w . logger . Debug ( "Received reader read, extending ping timer" , zap . Time ( "nextPingAt" , nextPingAt ) )
w . sendNextPingAt ( nextPingAt )
} else {
w . logger . Debug ( "Received reader close, shutting down" )
w . readNotifications = nil
// bye bye, we'll graceful shutdown because we deferred it
return
}
case raw := <- w . channel :
switch msg := raw . ( type ) {
case SocketClosed :
w . logger . Debug ( "Received close message, forwarding and shutting down" , zap . Object ( "msg" , msg ) )
w . sendClose ( msg )
// bye bye, we'll graceful shutdown because we deferred it
return
default :
w . logger . Debug ( "Received message, forwarding" , zap . Object ( "msg" , msg ) )
w . send ( msg )
}
case <- w . timer . C :
now := time . Now ( )
if now . After ( w . nextPingAt ) {
// We successfully passed the time when a ping should be sent! Let's send it!
w . sendPing ( )
// The timer doesn't need to be reactivated right now, so just zero out the next ping time.
w . nextPingAt = time . Time { }
} else {
// It's not time to send the ping yet - we got more reads in the meantime. Restart the timer with the new time-until-next-ping.
w . timer . Reset ( w . nextPingAt . Sub ( now ) )
}
}
w . logger . Debug ( "Awakening handled, resuming listening" )
}
}
// send actually transmits a ServerCommand to the client according to the protocol.
func ( w * writer ) send ( msg ServerCommand ) {
writeDeadline := time . Now ( ) . Add ( WriteTimeLimit )
w . logger . Debug ( "Setting deadline to write command" , zap . Time ( "writeDeadline" , writeDeadline ) )
err := w . conn . SetWriteDeadline ( writeDeadline )
if err != nil {
w . logger . Error ( "Error while setting write deadline" , zap . Time ( "writeDeadline" , writeDeadline ) , zap . Object ( "msg" , msg ) , zap . Error ( err ) )
}
w . logger . Debug ( "Opening message writer to send command" , zap . Object ( "msg" , msg ) )
writer , err := w . conn . NextWriter ( websocket . TextMessage )
if err != nil {
w . logger . Error ( "Error while getting writer from connection" , zap . Error ( err ) )
return
}
defer func ( writer io . WriteCloser ) {
w . logger . Debug ( "Closing message writer to send command" )
err := writer . Close ( )
if err != nil {
w . logger . Error ( "Error while closing writer to send command" , zap . Error ( err ) )
}
w . logger . Debug ( "Command sent" )
} ( writer )
w . logger . Debug ( "Marshaling command to JSON" , zap . Object ( "msg" , msg ) )
payload , err := json . Marshal ( msg )
if err != nil {
w . logger . Error ( "Error while rendering command payload to JSON" , zap . Object ( "msg" , msg ) , zap . Error ( err ) )
return
}
if len ( payload ) == 2 {
// This is an empty JSON message. We can leave it out.
w . logger . Debug ( "Empty payload, sending only command type" , zap . String ( "type" , string ( msg . ServerType ( ) ) ) )
_ , err = fmt . Fprintf ( writer , "%s!" , msg . ServerType ( ) )
if err != nil {
w . logger . Error ( "Error while writing no-payload command" , zap . Error ( err ) , zap . Object ( "msg" , msg ) )
}
} else {
// Because we need to send this, we put in a space instead of an exclamation mark.
w . logger . Debug ( "Sending command with payload" , zap . String ( "type" , string ( msg . ServerType ( ) ) ) , zap . ByteString ( "payload" , payload ) )
_ , err = fmt . Fprintf ( writer , "%s %s" , msg . ServerType ( ) , payload )
if err != nil {
w . logger . Error ( "Error while writing command with payload" , zap . Error ( err ) , zap . Object ( "msg" , msg ) )
}
}
}
// sendClose sends a close message on the websocket connection, but does not actually close the connection.
// It does, however, close the incoming message channel to the writer.
func ( w * writer ) sendClose ( msg SocketClosed ) {
w . logger . Debug ( "Shutting down the writer channel" )
close ( w . channel )
w . channel = nil
w . logger . Debug ( "Writing close message" )
err := w . conn . WriteControl ( websocket . CloseMessage , websocket . FormatCloseMessage ( int ( msg . Code ) , msg . Text ) , time . Now ( ) . Add ( ControlTimeLimit ) )
if err != nil {
w . logger . Warn ( "Error while sending close" , zap . Error ( err ) )
}
}
// sendPing sends a ping message on the websocket connection. The content is arbitrary.
func ( w * writer ) sendPing ( ) {
w . logger . Debug ( "Sending ping" )
err := w . conn . WriteControl ( websocket . PingMessage , [ ] byte ( PingData ) , time . Now ( ) . Add ( ControlTimeLimit ) )
if err != nil {
w . logger . Error ( "Error while sending ping" , zap . Error ( err ) )
}
}
// gracefulShutdown causes the writer to wait for the close handshake to finish and then shut down.
// It waits for the reader's readNotifications to close, indicating that it has also shut down, and for the channel to
// receive a SocketClosed message indicating that the main process has shut down.
// During this time, the writer ignores all other messages from the channel and sends no pings.
func ( w * writer ) gracefulShutdown ( ) {
defer w . finalShutdown ( )
// If the ping timer is still running, stop it and then close it.
if w . isTimerTicking ( ) && ! w . timer . Stop ( ) {
<- w . timer . C
}
w . timer = nil
w . nextPingAt = time . Time { }
w . logger . Debug ( "Waiting for all channels to shut down" )
for {
if w . channel == nil && w . readNotifications == nil {
w . logger . Debug ( "All channels closed, beginning final shutdown" )
// all done, we outta here, let the defer pick up the final shutdown
return
}
select {
case _ , open := <- w . readNotifications :
if ! open {
w . logger . Debug ( "Received reader close while shutting down" )
w . readNotifications = nil
}
case raw := <- w . channel :
switch msg := raw . ( type ) {
case SocketClosed :
w . logger . Debug ( "Received close message from channel while shutting down, forwarding" , zap . Object ( "msg" , msg ) )
w . sendClose ( msg )
default :
w . logger . Debug ( "Ignoring non-close message while shutting down" , zap . Object ( "msg" , msg ) )
}
}
}
}
// finalShutdown closes the socket and finishes cleanup.
func ( w * writer ) finalShutdown ( ) {
w . logger . Debug ( "Closing WebSocket connection" )
err := w . conn . Close ( )
if err != nil {
w . logger . Error ( "Received an error while closing" , zap . Error ( err ) )
}
w . logger . Debug ( "Shut down" )
}