mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-23 03:02:48 +01:00
Merge pull request #1469 from slingamn/alwayson_expiration.1
expiration for always-on clients
This commit is contained in:
commit
5c64612455
@ -500,6 +500,10 @@ accounts:
|
|||||||
# whether to mark always-on clients away when they have no active connections:
|
# whether to mark always-on clients away when they have no active connections:
|
||||||
auto-away: "opt-in"
|
auto-away: "opt-in"
|
||||||
|
|
||||||
|
# QUIT always-on clients from the server if they go this long without connecting
|
||||||
|
# (use 0 or omit for no expiration):
|
||||||
|
#always-on-expiration: 90d
|
||||||
|
|
||||||
# vhosts controls the assignment of vhosts (strings displayed in place of the user's
|
# vhosts controls the assignment of vhosts (strings displayed in place of the user's
|
||||||
# hostname/IP) by the HostServ service
|
# hostname/IP) by the HostServ service
|
||||||
vhosts:
|
vhosts:
|
||||||
|
@ -237,7 +237,7 @@ func (s *Session) EndMultilineBatch(label string) (batch MultilineBatch, err err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sets the session quit message, if there isn't one already
|
// sets the session quit message, if there isn't one already
|
||||||
func (sd *Session) SetQuitMessage(message string) (set bool) {
|
func (sd *Session) setQuitMessage(message string) (set bool) {
|
||||||
if message == "" {
|
if message == "" {
|
||||||
message = "Connection closed"
|
message = "Connection closed"
|
||||||
}
|
}
|
||||||
@ -443,6 +443,11 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToModes ma
|
|||||||
nextSessionID: 1,
|
nextSessionID: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if client.checkAlwaysOnExpirationNoMutex(config) {
|
||||||
|
server.logger.Debug("accounts", "always-on client not created due to expiration", account.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
client.SetMode(modes.TLS, true)
|
client.SetMode(modes.TLS, true)
|
||||||
for _, m := range uModes {
|
for _, m := range uModes {
|
||||||
client.SetMode(m, true)
|
client.SetMode(m, true)
|
||||||
@ -789,14 +794,16 @@ func (client *Client) Touch(session *Session) {
|
|||||||
var markDirty bool
|
var markDirty bool
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
if client.accountSettings.AutoreplayMissed || session.deviceID != "" {
|
if client.registered {
|
||||||
client.setLastSeen(now, session.deviceID)
|
client.updateIdleTimer(session, now)
|
||||||
if now.Sub(client.lastSeenLastWrite) > lastSeenWriteInterval {
|
if client.alwaysOn {
|
||||||
markDirty = true
|
client.setLastSeen(now, session.deviceID)
|
||||||
client.lastSeenLastWrite = now
|
if now.Sub(client.lastSeenLastWrite) > lastSeenWriteInterval {
|
||||||
|
markDirty = true
|
||||||
|
client.lastSeenLastWrite = now
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
client.updateIdleTimer(session, now)
|
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
if markDirty {
|
if markDirty {
|
||||||
client.markDirty(IncludeLastSeen)
|
client.markDirty(IncludeLastSeen)
|
||||||
@ -1364,7 +1371,7 @@ func (client *Client) Quit(message string, session *Session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, session := range sessions {
|
for _, session := range sessions {
|
||||||
if session.SetQuitMessage(message) {
|
if session.setQuitMessage(message) {
|
||||||
setFinalData(session)
|
setFinalData(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1378,6 +1385,7 @@ func (client *Client) destroy(session *Session) {
|
|||||||
config := client.server.Config()
|
config := client.server.Config()
|
||||||
var sessionsToDestroy []*Session
|
var sessionsToDestroy []*Session
|
||||||
var saveLastSeen bool
|
var saveLastSeen bool
|
||||||
|
var quitMessage string
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
|
|
||||||
@ -1390,6 +1398,13 @@ func (client *Client) destroy(session *Session) {
|
|||||||
// XXX a temporary (reattaching) client can be marked alwaysOn when it logs in,
|
// XXX a temporary (reattaching) client can be marked alwaysOn when it logs in,
|
||||||
// but then the session attaches to another client and we need to clean it up here
|
// but then the session attaches to another client and we need to clean it up here
|
||||||
alwaysOn := registered && client.alwaysOn
|
alwaysOn := registered && client.alwaysOn
|
||||||
|
// if we hit always-on-expiration, confirm the expiration and then proceed as though
|
||||||
|
// always-on is disabled:
|
||||||
|
if alwaysOn && session == nil && client.checkAlwaysOnExpirationNoMutex(config) {
|
||||||
|
quitMessage = "Timed out due to inactivity"
|
||||||
|
alwaysOn = false
|
||||||
|
client.alwaysOn = false
|
||||||
|
}
|
||||||
|
|
||||||
var remainingSessions int
|
var remainingSessions int
|
||||||
if session == nil {
|
if session == nil {
|
||||||
@ -1459,7 +1474,6 @@ func (client *Client) destroy(session *Session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// destroy all applicable sessions:
|
// destroy all applicable sessions:
|
||||||
var quitMessage string
|
|
||||||
for _, session := range sessionsToDestroy {
|
for _, session := range sessionsToDestroy {
|
||||||
if session.client != client {
|
if session.client != client {
|
||||||
// session has been attached to a new client; do not destroy it
|
// session has been attached to a new client; do not destroy it
|
||||||
@ -1468,7 +1482,7 @@ func (client *Client) destroy(session *Session) {
|
|||||||
session.stopIdleTimer()
|
session.stopIdleTimer()
|
||||||
// send quit/error message to client if they haven't been sent already
|
// send quit/error message to client if they haven't been sent already
|
||||||
client.Quit("", session)
|
client.Quit("", session)
|
||||||
quitMessage = session.quitMessage
|
quitMessage = session.quitMessage // doesn't need synch, we already detached
|
||||||
session.SetDestroyed()
|
session.SetDestroyed()
|
||||||
session.socket.Close()
|
session.socket.Close()
|
||||||
|
|
||||||
@ -1506,13 +1520,7 @@ func (client *Client) destroy(session *Session) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
splitQuitMessage := utils.MakeMessage(quitMessage)
|
var quitItem history.Item
|
||||||
quitItem := history.Item{
|
|
||||||
Type: history.Quit,
|
|
||||||
Nick: details.nickMask,
|
|
||||||
AccountName: details.accountName,
|
|
||||||
Message: splitQuitMessage,
|
|
||||||
}
|
|
||||||
var channels []*Channel
|
var channels []*Channel
|
||||||
// use a defer here to avoid writing to mysql while holding the destroy semaphore:
|
// use a defer here to avoid writing to mysql while holding the destroy semaphore:
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -1574,6 +1582,13 @@ func (client *Client) destroy(session *Session) {
|
|||||||
if quitMessage == "" {
|
if quitMessage == "" {
|
||||||
quitMessage = "Exited"
|
quitMessage = "Exited"
|
||||||
}
|
}
|
||||||
|
splitQuitMessage := utils.MakeMessage(quitMessage)
|
||||||
|
quitItem = history.Item{
|
||||||
|
Type: history.Quit,
|
||||||
|
Nick: details.nickMask,
|
||||||
|
AccountName: details.accountName,
|
||||||
|
Message: splitQuitMessage,
|
||||||
|
}
|
||||||
var cache MessageCache
|
var cache MessageCache
|
||||||
cache.Initialize(client.server, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
|
cache.Initialize(client.server, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
|
||||||
for friend := range friends {
|
for friend := range friends {
|
||||||
|
@ -223,10 +223,11 @@ func historyEnabled(serverSetting PersistentStatus, localSetting HistoryStatus)
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MulticlientConfig struct {
|
type MulticlientConfig struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
AllowedByDefault bool `yaml:"allowed-by-default"`
|
AllowedByDefault bool `yaml:"allowed-by-default"`
|
||||||
AlwaysOn PersistentStatus `yaml:"always-on"`
|
AlwaysOn PersistentStatus `yaml:"always-on"`
|
||||||
AutoAway PersistentStatus `yaml:"auto-away"`
|
AutoAway PersistentStatus `yaml:"auto-away"`
|
||||||
|
AlwaysOnExpiration custime.Duration `yaml:"always-on-expiration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type throttleConfig struct {
|
type throttleConfig struct {
|
||||||
|
@ -337,26 +337,19 @@ func (client *Client) AccountSettings() (result AccountSettings) {
|
|||||||
|
|
||||||
func (client *Client) SetAccountSettings(settings AccountSettings) {
|
func (client *Client) SetAccountSettings(settings AccountSettings) {
|
||||||
// we mark dirty if the client is transitioning to always-on
|
// we mark dirty if the client is transitioning to always-on
|
||||||
var becameAlwaysOn, autoreplayMissedDisabled bool
|
var becameAlwaysOn bool
|
||||||
alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
|
alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
if client.registered {
|
if client.registered {
|
||||||
// only allow the client to become always-on if their nick equals their account name
|
// only allow the client to become always-on if their nick equals their account name
|
||||||
alwaysOn = alwaysOn && client.nick == client.accountName
|
alwaysOn = alwaysOn && client.nick == client.accountName
|
||||||
autoreplayMissedDisabled = (client.accountSettings.AutoreplayMissed && !settings.AutoreplayMissed)
|
|
||||||
becameAlwaysOn = (!client.alwaysOn && alwaysOn)
|
becameAlwaysOn = (!client.alwaysOn && alwaysOn)
|
||||||
client.alwaysOn = alwaysOn
|
client.alwaysOn = alwaysOn
|
||||||
if autoreplayMissedDisabled {
|
|
||||||
// clear the lastSeen entry for the default session, but not for device IDs
|
|
||||||
delete(client.lastSeen, "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
client.accountSettings = settings
|
client.accountSettings = settings
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
if becameAlwaysOn {
|
if becameAlwaysOn {
|
||||||
client.markDirty(IncludeAllAttrs)
|
client.markDirty(IncludeAllAttrs)
|
||||||
} else if autoreplayMissedDisabled {
|
|
||||||
client.markDirty(IncludeLastSeen)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -449,6 +442,29 @@ func (client *Client) Realname() string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) IsExpiredAlwaysOn(config *Config) (result bool) {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
defer client.stateMutex.Unlock()
|
||||||
|
return client.checkAlwaysOnExpirationNoMutex(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) checkAlwaysOnExpirationNoMutex(config *Config) (result bool) {
|
||||||
|
if !(client.registered && client.alwaysOn) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
deadline := time.Duration(config.Accounts.Multiclient.AlwaysOnExpiration)
|
||||||
|
if deadline == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
for _, ts := range client.lastSeen {
|
||||||
|
if now.Sub(ts) < deadline {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (channel *Channel) Name() string {
|
func (channel *Channel) Name() string {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
defer channel.stateMutex.RUnlock()
|
defer channel.stateMutex.RUnlock()
|
||||||
|
@ -154,8 +154,11 @@ func (m *MessageCache) InitializeSplitMessage(server *Server, nickmask, accountN
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we need to send the same batch ID to all recipient sessions;
|
// we need to send the same batch ID to all recipient sessions;
|
||||||
// use a uuidv4-alike to ensure that it won't collide
|
// ensure it doesn't collide. a half-sized token has 64 bits of entropy,
|
||||||
batch := composeMultilineBatch(utils.GenerateSecretToken(), nickmask, accountName, tags, command, target, message)
|
// so a collision isn't expected until there are on the order of 2**32
|
||||||
|
// concurrent batches being relayed:
|
||||||
|
batchID := utils.GenerateSecretToken()[:utils.SecretTokenLength/2]
|
||||||
|
batch := composeMultilineBatch(batchID, nickmask, accountName, tags, command, target, message)
|
||||||
m.fullTagsMultiline = make([][]byte, len(batch))
|
m.fullTagsMultiline = make([][]byte, len(batch))
|
||||||
for i, msg := range batch {
|
for i, msg := range batch {
|
||||||
if forceTrailing {
|
if forceTrailing {
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -33,6 +34,10 @@ import (
|
|||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
alwaysOnExpirationPollPeriod = time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// common error line to sub values into
|
// common error line to sub values into
|
||||||
errorMsg = "ERROR :%s\r\n"
|
errorMsg = "ERROR :%s\r\n"
|
||||||
@ -114,6 +119,8 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
|||||||
signal.Notify(server.signals, ServerExitSignals...)
|
signal.Notify(server.signals, ServerExitSignals...)
|
||||||
signal.Notify(server.rehashSignal, syscall.SIGHUP)
|
signal.Notify(server.rehashSignal, syscall.SIGHUP)
|
||||||
|
|
||||||
|
time.AfterFunc(alwaysOnExpirationPollPeriod, server.handleAlwaysOnExpirations)
|
||||||
|
|
||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,6 +234,31 @@ func (server *Server) checkTorLimits() (banned bool, message string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (server *Server) handleAlwaysOnExpirations() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
server.logger.Error("internal",
|
||||||
|
fmt.Sprintf("Panic in always-on cleanup: %v\n%s", r, debug.Stack()))
|
||||||
|
}
|
||||||
|
// either way, reschedule
|
||||||
|
time.AfterFunc(alwaysOnExpirationPollPeriod, server.handleAlwaysOnExpirations)
|
||||||
|
}()
|
||||||
|
|
||||||
|
config := server.Config()
|
||||||
|
deadline := time.Duration(config.Accounts.Multiclient.AlwaysOnExpiration)
|
||||||
|
if deadline == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
server.logger.Info("accounts", "Checking always-on clients for expiration")
|
||||||
|
for _, client := range server.clients.AllClients() {
|
||||||
|
if client.IsExpiredAlwaysOn(config) {
|
||||||
|
// TODO save the channels list, use it for autojoin if/when they return?
|
||||||
|
server.logger.Info("accounts", "Expiring always-on client", client.AccountName())
|
||||||
|
client.destroy(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// server functionality
|
// server functionality
|
||||||
//
|
//
|
||||||
|
2
irctest
2
irctest
@ -1 +1 @@
|
|||||||
Subproject commit 0b9087cc3959a558aca407565f17d197669585bf
|
Subproject commit 307722fbecc5ab69ee3246153b8f8f91ad830830
|
@ -472,6 +472,10 @@ accounts:
|
|||||||
# whether to mark always-on clients away when they have no active connections:
|
# whether to mark always-on clients away when they have no active connections:
|
||||||
auto-away: "opt-in"
|
auto-away: "opt-in"
|
||||||
|
|
||||||
|
# QUIT always-on clients from the server if they go this long without connecting
|
||||||
|
# (use 0 or omit for no expiration):
|
||||||
|
#always-on-expiration: 90d
|
||||||
|
|
||||||
# vhosts controls the assignment of vhosts (strings displayed in place of the user's
|
# vhosts controls the assignment of vhosts (strings displayed in place of the user's
|
||||||
# hostname/IP) by the HostServ service
|
# hostname/IP) by the HostServ service
|
||||||
vhosts:
|
vhosts:
|
||||||
|
Loading…
Reference in New Issue
Block a user