mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 18:52:41 +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:
|
||||
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
|
||||
# hostname/IP) by the HostServ service
|
||||
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
|
||||
func (sd *Session) SetQuitMessage(message string) (set bool) {
|
||||
func (sd *Session) setQuitMessage(message string) (set bool) {
|
||||
if message == "" {
|
||||
message = "Connection closed"
|
||||
}
|
||||
@ -443,6 +443,11 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToModes ma
|
||||
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)
|
||||
for _, m := range uModes {
|
||||
client.SetMode(m, true)
|
||||
@ -789,14 +794,16 @@ func (client *Client) Touch(session *Session) {
|
||||
var markDirty bool
|
||||
now := time.Now().UTC()
|
||||
client.stateMutex.Lock()
|
||||
if client.accountSettings.AutoreplayMissed || session.deviceID != "" {
|
||||
if client.registered {
|
||||
client.updateIdleTimer(session, now)
|
||||
if client.alwaysOn {
|
||||
client.setLastSeen(now, session.deviceID)
|
||||
if now.Sub(client.lastSeenLastWrite) > lastSeenWriteInterval {
|
||||
markDirty = true
|
||||
client.lastSeenLastWrite = now
|
||||
}
|
||||
}
|
||||
client.updateIdleTimer(session, now)
|
||||
}
|
||||
client.stateMutex.Unlock()
|
||||
if markDirty {
|
||||
client.markDirty(IncludeLastSeen)
|
||||
@ -1364,7 +1371,7 @@ func (client *Client) Quit(message string, session *Session) {
|
||||
}
|
||||
|
||||
for _, session := range sessions {
|
||||
if session.SetQuitMessage(message) {
|
||||
if session.setQuitMessage(message) {
|
||||
setFinalData(session)
|
||||
}
|
||||
}
|
||||
@ -1378,6 +1385,7 @@ func (client *Client) destroy(session *Session) {
|
||||
config := client.server.Config()
|
||||
var sessionsToDestroy []*Session
|
||||
var saveLastSeen bool
|
||||
var quitMessage string
|
||||
|
||||
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,
|
||||
// but then the session attaches to another client and we need to clean it up here
|
||||
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
|
||||
if session == nil {
|
||||
@ -1459,7 +1474,6 @@ func (client *Client) destroy(session *Session) {
|
||||
}
|
||||
|
||||
// destroy all applicable sessions:
|
||||
var quitMessage string
|
||||
for _, session := range sessionsToDestroy {
|
||||
if session.client != client {
|
||||
// session has been attached to a new client; do not destroy it
|
||||
@ -1468,7 +1482,7 @@ func (client *Client) destroy(session *Session) {
|
||||
session.stopIdleTimer()
|
||||
// send quit/error message to client if they haven't been sent already
|
||||
client.Quit("", session)
|
||||
quitMessage = session.quitMessage
|
||||
quitMessage = session.quitMessage // doesn't need synch, we already detached
|
||||
session.SetDestroyed()
|
||||
session.socket.Close()
|
||||
|
||||
@ -1506,13 +1520,7 @@ func (client *Client) destroy(session *Session) {
|
||||
return
|
||||
}
|
||||
|
||||
splitQuitMessage := utils.MakeMessage(quitMessage)
|
||||
quitItem := history.Item{
|
||||
Type: history.Quit,
|
||||
Nick: details.nickMask,
|
||||
AccountName: details.accountName,
|
||||
Message: splitQuitMessage,
|
||||
}
|
||||
var quitItem history.Item
|
||||
var channels []*Channel
|
||||
// use a defer here to avoid writing to mysql while holding the destroy semaphore:
|
||||
defer func() {
|
||||
@ -1574,6 +1582,13 @@ func (client *Client) destroy(session *Session) {
|
||||
if quitMessage == "" {
|
||||
quitMessage = "Exited"
|
||||
}
|
||||
splitQuitMessage := utils.MakeMessage(quitMessage)
|
||||
quitItem = history.Item{
|
||||
Type: history.Quit,
|
||||
Nick: details.nickMask,
|
||||
AccountName: details.accountName,
|
||||
Message: splitQuitMessage,
|
||||
}
|
||||
var cache MessageCache
|
||||
cache.Initialize(client.server, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
|
||||
for friend := range friends {
|
||||
|
@ -227,6 +227,7 @@ type MulticlientConfig struct {
|
||||
AllowedByDefault bool `yaml:"allowed-by-default"`
|
||||
AlwaysOn PersistentStatus `yaml:"always-on"`
|
||||
AutoAway PersistentStatus `yaml:"auto-away"`
|
||||
AlwaysOnExpiration custime.Duration `yaml:"always-on-expiration"`
|
||||
}
|
||||
|
||||
type throttleConfig struct {
|
||||
|
@ -337,26 +337,19 @@ func (client *Client) AccountSettings() (result AccountSettings) {
|
||||
|
||||
func (client *Client) SetAccountSettings(settings AccountSettings) {
|
||||
// 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)
|
||||
client.stateMutex.Lock()
|
||||
if client.registered {
|
||||
// only allow the client to become always-on if their nick equals their account name
|
||||
alwaysOn = alwaysOn && client.nick == client.accountName
|
||||
autoreplayMissedDisabled = (client.accountSettings.AutoreplayMissed && !settings.AutoreplayMissed)
|
||||
becameAlwaysOn = (!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.stateMutex.Unlock()
|
||||
if becameAlwaysOn {
|
||||
client.markDirty(IncludeAllAttrs)
|
||||
} else if autoreplayMissedDisabled {
|
||||
client.markDirty(IncludeLastSeen)
|
||||
}
|
||||
}
|
||||
|
||||
@ -449,6 +442,29 @@ func (client *Client) Realname() string {
|
||||
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 {
|
||||
channel.stateMutex.RLock()
|
||||
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;
|
||||
// use a uuidv4-alike to ensure that it won't collide
|
||||
batch := composeMultilineBatch(utils.GenerateSecretToken(), nickmask, accountName, tags, command, target, message)
|
||||
// ensure it doesn't collide. a half-sized token has 64 bits of entropy,
|
||||
// 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))
|
||||
for i, msg := range batch {
|
||||
if forceTrailing {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -33,6 +34,10 @@ import (
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
const (
|
||||
alwaysOnExpirationPollPeriod = time.Hour
|
||||
)
|
||||
|
||||
var (
|
||||
// common error line to sub values into
|
||||
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.rehashSignal, syscall.SIGHUP)
|
||||
|
||||
time.AfterFunc(alwaysOnExpirationPollPeriod, server.handleAlwaysOnExpirations)
|
||||
|
||||
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
|
||||
//
|
||||
|
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:
|
||||
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
|
||||
# hostname/IP) by the HostServ service
|
||||
vhosts:
|
||||
|
Loading…
Reference in New Issue
Block a user