diff --git a/client/src/neko/base.ts b/client/src/neko/base.ts index 6068fcd54..12828595d 100644 --- a/client/src/neko/base.ts +++ b/client/src/neko/base.ts @@ -20,6 +20,7 @@ export interface BaseEvents { export abstract class BaseClient extends EventEmitter { protected _ws?: WebSocket + protected _ws_heartbeat?: number protected _peer?: RTCPeerConnection protected _channel?: RTCDataChannel protected _timeout?: number @@ -82,6 +83,11 @@ export abstract class BaseClient extends EventEmitter { this._timeout = undefined } + if (this._ws_heartbeat) { + clearInterval(this._ws_heartbeat) + this._ws_heartbeat = undefined + } + if (this._ws) { // reset all events this._ws.onmessage = () => {} diff --git a/client/src/neko/events.ts b/client/src/neko/events.ts index 5deb010a6..45c264db3 100644 --- a/client/src/neko/events.ts +++ b/client/src/neko/events.ts @@ -14,6 +14,9 @@ export const EVENT = { DISCONNECT: 'system/disconnect', ERROR: 'system/error', }, + CLIENT: { + HEARTBEAT: 'client/heartbeat' + }, SIGNAL: { OFFER: 'signal/offer', ANSWER: 'signal/answer', @@ -69,6 +72,7 @@ export type Events = typeof EVENT export type WebSocketEvents = | SystemEvents + | ClientEvents | ControlEvents | MemberEvents | SignalEvents @@ -87,6 +91,7 @@ export type ControlEvents = | typeof EVENT.CONTROL.KEYBOARD export type SystemEvents = typeof EVENT.SYSTEM.DISCONNECT +export type ClientEvents = typeof EVENT.CLIENT.HEARTBEAT export type MemberEvents = typeof EVENT.MEMBER.LIST | typeof EVENT.MEMBER.CONNECTED | typeof EVENT.MEMBER.DISCONNECTED export type SignalEvents = diff --git a/client/src/neko/index.ts b/client/src/neko/index.ts index 16ca0b6fa..d40fa568b 100644 --- a/client/src/neko/index.ts +++ b/client/src/neko/index.ts @@ -134,7 +134,7 @@ export class NekoClient extends BaseClient implements EventEmitter { ///////////////////////////// // System Events ///////////////////////////// - protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks, file_transfer }: SystemInitPayload) { + protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks, file_transfer, heartbeat_interval }: SystemInitPayload) { this.$accessor.remote.setImplicitHosting(implicit_hosting) this.$accessor.remote.setFileTransfer(file_transfer) @@ -145,6 +145,11 @@ export class NekoClient extends BaseClient implements EventEmitter { id: locks[resource], }) } + + if (heartbeat_interval > 0) { + if (this._ws_heartbeat) clearInterval(this._ws_heartbeat) + this._ws_heartbeat = window.setInterval(() => this.sendMessage(EVENT.CLIENT.HEARTBEAT), heartbeat_interval * 1000) + } } protected [EVENT.SYSTEM.DISCONNECT]({ message }: SystemMessagePayload) { diff --git a/client/src/neko/messages.ts b/client/src/neko/messages.ts index 54d1cb11b..ac12ef23f 100644 --- a/client/src/neko/messages.ts +++ b/client/src/neko/messages.ts @@ -61,6 +61,7 @@ export interface SystemInitPayload { implicit_hosting: boolean locks: Record file_transfer: boolean + heartbeat_interval: number } // system/disconnect diff --git a/server/internal/config/session.go b/server/internal/config/session.go index 08934ab4d..1414171e4 100644 --- a/server/internal/config/session.go +++ b/server/internal/config/session.go @@ -18,6 +18,7 @@ type Session struct { ImplicitHosting bool InactiveCursors bool MercifulReconnect bool + HeartbeatInterval int APIToken string CookieEnabled bool @@ -67,6 +68,11 @@ func (Session) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Int("session.heartbeat_interval", 120, "interval in seconds for sending heartbeat messages") + if err := viper.BindPFlag("session.heartbeat_interval", cmd.PersistentFlags().Lookup("session.heartbeat_interval")); err != nil { + return err + } + cmd.PersistentFlags().String("session.api_token", "", "API token for interacting with external services") if err := viper.BindPFlag("session.api_token", cmd.PersistentFlags().Lookup("session.api_token")); err != nil { return err @@ -112,6 +118,11 @@ func (Session) InitV2(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Int("heartbeat_interval", 120, "heartbeat interval in seconds") + if err := viper.BindPFlag("heartbeat_interval", cmd.PersistentFlags().Lookup("heartbeat_interval")); err != nil { + return err + } + return nil } @@ -125,6 +136,7 @@ func (s *Session) Set() { s.ImplicitHosting = viper.GetBool("session.implicit_hosting") s.InactiveCursors = viper.GetBool("session.inactive_cursors") s.MercifulReconnect = viper.GetBool("session.merciful_reconnect") + s.HeartbeatInterval = viper.GetInt("session.heartbeat_interval") s.APIToken = viper.GetString("session.api_token") s.CookieEnabled = viper.GetBool("session.cookie.enabled") @@ -156,4 +168,8 @@ func (s *Session) SetV2() { s.ControlProtection = viper.GetBool("control_protection") log.Warn().Msg("you are using v2 configuration 'NEKO_CONTROL_PROTECTION' which is deprecated, please use 'NEKO_SESSION_CONTROL_PROTECTION' instead") } + if viper.IsSet("heartbeat_interval") { + s.HeartbeatInterval = viper.GetInt("heartbeat_interval") + log.Warn().Msg("you are using v2 configuration 'NEKO_HEARTBEAT_INTERVAL' which is deprecated, please use 'NEKO_SESSION_HEARTBEAT_INTERVAL' instead") + } } diff --git a/server/internal/http/legacy/event/events.go b/server/internal/http/legacy/event/events.go index 8032ba90f..3cc0b3aec 100644 --- a/server/internal/http/legacy/event/events.go +++ b/server/internal/http/legacy/event/events.go @@ -6,6 +6,10 @@ const ( SYSTEM_ERROR = "system/error" ) +const ( + CLIENT_HEARTBEAT = "client/heartbeat" +) + const ( SIGNAL_OFFER = "signal/offer" SIGNAL_ANSWER = "signal/answer" diff --git a/server/internal/http/legacy/message/messages.go b/server/internal/http/legacy/message/messages.go index 83921e669..708f89a9b 100644 --- a/server/internal/http/legacy/message/messages.go +++ b/server/internal/http/legacy/message/messages.go @@ -11,10 +11,11 @@ type Message struct { } type SystemInit struct { - Event string `json:"event"` - Locks map[string]string `json:"locks"` - ImplicitHosting bool `json:"implicit_hosting"` - FileTransfer bool `json:"file_transfer"` + Event string `json:"event"` + Locks map[string]string `json:"locks"` + ImplicitHosting bool `json:"implicit_hosting"` + FileTransfer bool `json:"file_transfer"` + HeartbeatInterval int `json:"heartbeat_interval"` } type SystemMessage struct { diff --git a/server/internal/http/legacy/wstobackend.go b/server/internal/http/legacy/wstobackend.go index 8ab4cd186..114fd5fbb 100644 --- a/server/internal/http/legacy/wstobackend.go +++ b/server/internal/http/legacy/wstobackend.go @@ -26,6 +26,11 @@ func (s *session) wsToBackend(msg []byte) error { } switch header.Event { + // Client Events + case oldEvent.CLIENT_HEARTBEAT: + // do nothing + return nil + // Signal Events case oldEvent.SIGNAL_OFFER: request := &oldMessage.SignalOffer{} diff --git a/server/internal/http/legacy/wstoclient.go b/server/internal/http/legacy/wstoclient.go index b3e877911..9e86d0556 100644 --- a/server/internal/http/legacy/wstoclient.go +++ b/server/internal/http/legacy/wstoclient.go @@ -259,7 +259,8 @@ func (s *session) wsToClient(msg []byte) error { ImplicitHosting: request.Settings.ImplicitHosting, Locks: locks, // TODO: hack - we don't know if file transfer is enabled, we would need to check the global config. - FileTransfer: viper.GetBool("filetransfer.enabled") || (viper.GetBool("legacy") && viper.GetBool("file_transfer_enabled")), + FileTransfer: viper.GetBool("filetransfer.enabled") || (viper.GetBool("legacy") && viper.GetBool("file_transfer_enabled")), + HeartbeatInterval: request.Settings.HeartbeatInterval, }) case event.SYSTEM_ADMIN: diff --git a/server/internal/session/manager.go b/server/internal/session/manager.go index 29bb8e4b1..40f1470d1 100644 --- a/server/internal/session/manager.go +++ b/server/internal/session/manager.go @@ -27,6 +27,7 @@ func New(config *config.Session) *SessionManagerCtx { ImplicitHosting: config.ImplicitHosting, InactiveCursors: config.InactiveCursors, MercifulReconnect: config.MercifulReconnect, + HeartbeatInterval: config.HeartbeatInterval, }, tokens: make(map[string]string), sessions: make(map[string]*SessionCtx), diff --git a/server/internal/websocket/handler/handler.go b/server/internal/websocket/handler/handler.go index 576584a56..22634ec0d 100644 --- a/server/internal/websocket/handler/handler.go +++ b/server/internal/websocket/handler/handler.go @@ -36,6 +36,10 @@ type MessageHandlerCtx struct { func (h *MessageHandlerCtx) Message(session types.Session, data types.WebSocketMessage) bool { var err error switch data.Event { + // Client Events + case event.CLIENT_HEARTBEAT: + // do nothing + // System Events case event.SYSTEM_LOGS: payload := &message.SystemLogs{} diff --git a/server/internal/websocket/manager.go b/server/internal/websocket/manager.go index 1d09fad45..b34e3eda6 100644 --- a/server/internal/websocket/manager.go +++ b/server/internal/websocket/manager.go @@ -31,8 +31,9 @@ const maxPayloadLogLength = 10_000 var nologEvents = []string{ // don't log twice event.SYSTEM_LOGS, - // don't log heartbeat + // don't log heartbeats event.SYSTEM_HEARTBEAT, + event.CLIENT_HEARTBEAT, // don't log every cursor update event.SESSION_CURSORS, } diff --git a/server/pkg/types/event/events.go b/server/pkg/types/event/events.go index 0259d4d79..91f005f87 100644 --- a/server/pkg/types/event/events.go +++ b/server/pkg/types/event/events.go @@ -9,6 +9,10 @@ const ( SYSTEM_HEARTBEAT = "system/heartbeat" ) +const ( + CLIENT_HEARTBEAT = "client/heartbeat" +) + const ( SIGNAL_REQUEST = "signal/request" SIGNAL_RESTART = "signal/restart" diff --git a/server/pkg/types/session.go b/server/pkg/types/session.go index fba64d8cb..25aa399af 100644 --- a/server/pkg/types/session.go +++ b/server/pkg/types/session.go @@ -47,6 +47,7 @@ type Settings struct { ImplicitHosting bool `json:"implicit_hosting"` InactiveCursors bool `json:"inactive_cursors"` MercifulReconnect bool `json:"merciful_reconnect"` + HeartbeatInterval int `json:"heartbeat_interval"` // plugin scope Plugins PluginSettings `json:"plugins"`