X-Git-Url: https://git.r.bdr.sh/rbdr/mobius/blobdiff_plain/c1c44744fdbd3ddeec509efffba91011a4d49990..b4d84cd8dc5e3d3bdbe84072bf5129ea2bcc3e49:/hotline/server.go?ds=sidebyside diff --git a/hotline/server.go b/hotline/server.go index 06a0695..b17102e 100644 --- a/hotline/server.go +++ b/hotline/server.go @@ -2,7 +2,6 @@ package hotline import ( "bufio" - "bytes" "context" "encoding/binary" "errors" @@ -33,14 +32,6 @@ type requestCtx struct { name string } -const ( - userIdleSeconds = 300 // time in seconds before an inactive user is marked idle - idleCheckInterval = 10 // time in seconds to check for idle users - trackerUpdateFrequency = 300 // time in seconds between tracker re-registration -) - -var nostalgiaVersion = []byte{0, 0, 2, 0x2c} // version ID used by the Nostalgia client - type Server struct { Port int Accounts map[string]*Account @@ -57,7 +48,9 @@ type Server struct { NextGuestID *uint16 TrackerPassID [4]byte - Stats *Stats + + StatsMu sync.Mutex + Stats *Stats FS FileStore // Storage backend to use for File storage @@ -74,6 +67,16 @@ type Server struct { banList map[string]*time.Time } +func (s *Server) CurrentStats() Stats { + s.StatsMu.Lock() + defer s.StatsMu.Unlock() + + stats := s.Stats + stats.CurrentlyConnected = len(s.Clients) + + return *stats +} + type PrivateChat struct { Subject string ClientConn map[uint16]*ClientConn @@ -146,18 +149,18 @@ func (s *Server) sendTransaction(t Transaction) error { s.mux.Lock() client := s.Clients[uint16(clientID)] + s.mux.Unlock() if client == nil { return fmt.Errorf("invalid client id %v", *t.clientID) } - s.mux.Unlock() - b, err := t.MarshalBinary() if err != nil { return err } - if _, err := client.Connection.Write(b); err != nil { + _, err = client.Connection.Write(b) + if err != nil { return err } @@ -219,7 +222,7 @@ func NewServer(configDir string, netPort int, logger *zap.SugaredLogger, FS File Logger: logger, NextGuestID: new(uint16), outbox: make(chan Transaction), - Stats: &Stats{StartTime: time.Now()}, + Stats: &Stats{Since: time.Now()}, ThreadedNews: &ThreadedNews{}, FS: FS, banList: make(map[string]*time.Time), @@ -374,7 +377,6 @@ func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *Clie Version: []byte{}, AutoReply: []byte{}, transfers: map[int]map[[4]byte]*FileTransfer{}, - Agreed: false, RemoteAddr: remoteAddr, } clientConn.transfers = map[int]map[[4]byte]*FileTransfer{ @@ -461,9 +463,6 @@ func (s *Server) connectedUsers() []Field { var connectedUsers []Field for _, c := range sortedClients(s.Clients) { - if !c.Agreed { - continue - } user := User{ ID: *c.ID, Icon: c.Icon, @@ -558,8 +557,13 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser scanner.Scan() + // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the + // scanner re-uses the buffer for subsequent scans. + buf := make([]byte, len(scanner.Bytes())) + copy(buf, scanner.Bytes()) + var clientLogin Transaction - if _, err := clientLogin.Write(scanner.Bytes()); err != nil { + if _, err := clientLogin.Write(buf); err != nil { return err } @@ -661,25 +665,26 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser } // Used simplified hotline v1.2.3 login flow for clients that do not send login info in tranAgreed - if c.Version == nil || bytes.Equal(c.Version, nostalgiaVersion) { - c.Agreed = true - c.logger = c.logger.With("name", string(c.UserName)) - c.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%v", func() int { i, _ := byteToInt(c.Version); return i }())) - - for _, t := range c.notifyOthers( - *NewTransaction( - tranNotifyChangeUser, nil, - NewField(fieldUserName, c.UserName), - NewField(fieldUserID, *c.ID), - NewField(fieldUserIconID, c.Icon), - NewField(fieldUserFlags, c.Flags), - ), - ) { - c.Server.outbox <- t - } + // TODO: figure out a generalized solution that doesn't require playing whack-a-mole for specific client versions + c.logger = c.logger.With("name", string(c.UserName)) + c.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%v", func() int { i, _ := byteToInt(c.Version); return i }())) + + for _, t := range c.notifyOthers( + *NewTransaction( + tranNotifyChangeUser, nil, + NewField(fieldUserName, c.UserName), + NewField(fieldUserID, *c.ID), + NewField(fieldUserIconID, c.Icon), + NewField(fieldUserFlags, c.Flags), + ), + ) { + c.Server.outbox <- t } - c.Server.Stats.LoginCount += 1 + c.Server.Stats.ConnectionCounter += 1 + if len(s.Clients) > c.Server.Stats.ConnectionPeak { + c.Server.Stats.ConnectionPeak = len(s.Clients) + } // Scan for new transactions and handle them as they come in. for scanner.Scan() { @@ -739,6 +744,10 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro delete(s.fileTransfers, t.ReferenceNumber) s.mux.Unlock() + // Wait a few seconds before closing the connection: this is a workaround for problems + // observed with Windows clients where the client must initiate close of the TCP connection before + // the server does. This is gross and seems unnecessary. TODO: Revisit? + time.Sleep(3 * time.Second) }() s.mux.Lock() @@ -772,6 +781,10 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro } case FileDownload: s.Stats.DownloadCounter += 1 + s.Stats.DownloadsInProgress += 1 + defer func() { + s.Stats.DownloadsInProgress -= 1 + }() var dataOffset int64 if fileTransfer.fileResumeData != nil { @@ -826,6 +839,8 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro case FileUpload: s.Stats.UploadCounter += 1 + s.Stats.UploadsInProgress += 1 + defer func() { s.Stats.UploadsInProgress -= 1 }() var file *os.File @@ -882,7 +897,12 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro } rLogger.Infow("File upload complete", "dstFile", fullPath) + case FolderDownload: + s.Stats.DownloadCounter += 1 + s.Stats.DownloadsInProgress += 1 + defer func() { s.Stats.DownloadsInProgress -= 1 }() + // Folder Download flow: // 1. Get filePath from the transfer // 2. Iterate over files @@ -1046,6 +1066,9 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro } case FolderUpload: + s.Stats.UploadCounter += 1 + s.Stats.UploadsInProgress += 1 + defer func() { s.Stats.UploadsInProgress -= 1 }() rLogger.Infow( "Folder upload started", "dstPath", fullPath,