Explorar el Código

Implement expiration for always-on clients

Fixes #810
tags/v2.5.0-rc1
Shivaram Lingamneni hace 3 años
padre
commit
48166b5b4b
Se han modificado 6 ficheros con 101 adiciones y 29 borrados
  1. 4
    0
      default.yaml
  2. 32
    17
      irc/client.go
  3. 5
    4
      irc/config.go
  4. 24
    8
      irc/getters.go
  5. 32
    0
      irc/server.go
  6. 4
    0
      traditional.yaml

+ 4
- 0
default.yaml Ver fichero

@@ -500,6 +500,10 @@ accounts:
500 500
         # whether to mark always-on clients away when they have no active connections:
501 501
         auto-away: "opt-in"
502 502
 
503
+        # QUIT always-on clients from the server if they go this long without connecting
504
+        # (use 0 or omit for no expiration):
505
+        #always-on-expiration: 90d
506
+
503 507
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
504 508
     # hostname/IP) by the HostServ service
505 509
     vhosts:

+ 32
- 17
irc/client.go Ver fichero

@@ -237,7 +237,7 @@ func (s *Session) EndMultilineBatch(label string) (batch MultilineBatch, err err
237 237
 }
238 238
 
239 239
 // sets the session quit message, if there isn't one already
240
-func (sd *Session) SetQuitMessage(message string) (set bool) {
240
+func (sd *Session) setQuitMessage(message string) (set bool) {
241 241
 	if message == "" {
242 242
 		message = "Connection closed"
243 243
 	}
@@ -443,6 +443,11 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToModes ma
443 443
 		nextSessionID: 1,
444 444
 	}
445 445
 
446
+	if client.checkAlwaysOnExpirationNoMutex(config) {
447
+		server.logger.Debug("accounts", "always-on client not created due to expiration", account.Name)
448
+		return
449
+	}
450
+
446 451
 	client.SetMode(modes.TLS, true)
447 452
 	for _, m := range uModes {
448 453
 		client.SetMode(m, true)
@@ -789,14 +794,16 @@ func (client *Client) Touch(session *Session) {
789 794
 	var markDirty bool
790 795
 	now := time.Now().UTC()
791 796
 	client.stateMutex.Lock()
792
-	if client.accountSettings.AutoreplayMissed || session.deviceID != "" {
793
-		client.setLastSeen(now, session.deviceID)
794
-		if now.Sub(client.lastSeenLastWrite) > lastSeenWriteInterval {
795
-			markDirty = true
796
-			client.lastSeenLastWrite = now
797
+	if client.registered {
798
+		client.updateIdleTimer(session, now)
799
+		if client.alwaysOn {
800
+			client.setLastSeen(now, session.deviceID)
801
+			if now.Sub(client.lastSeenLastWrite) > lastSeenWriteInterval {
802
+				markDirty = true
803
+				client.lastSeenLastWrite = now
804
+			}
797 805
 		}
798 806
 	}
799
-	client.updateIdleTimer(session, now)
800 807
 	client.stateMutex.Unlock()
801 808
 	if markDirty {
802 809
 		client.markDirty(IncludeLastSeen)
@@ -1364,7 +1371,7 @@ func (client *Client) Quit(message string, session *Session) {
1364 1371
 	}
1365 1372
 
1366 1373
 	for _, session := range sessions {
1367
-		if session.SetQuitMessage(message) {
1374
+		if session.setQuitMessage(message) {
1368 1375
 			setFinalData(session)
1369 1376
 		}
1370 1377
 	}
@@ -1378,6 +1385,7 @@ func (client *Client) destroy(session *Session) {
1378 1385
 	config := client.server.Config()
1379 1386
 	var sessionsToDestroy []*Session
1380 1387
 	var saveLastSeen bool
1388
+	var quitMessage string
1381 1389
 
1382 1390
 	client.stateMutex.Lock()
1383 1391
 
@@ -1390,6 +1398,13 @@ func (client *Client) destroy(session *Session) {
1390 1398
 	// XXX a temporary (reattaching) client can be marked alwaysOn when it logs in,
1391 1399
 	// but then the session attaches to another client and we need to clean it up here
1392 1400
 	alwaysOn := registered && client.alwaysOn
1401
+	// if we hit always-on-expiration, confirm the expiration and then proceed as though
1402
+	// always-on is disabled:
1403
+	if alwaysOn && session == nil && client.checkAlwaysOnExpirationNoMutex(config) {
1404
+		quitMessage = "Timed out due to inactivity"
1405
+		alwaysOn = false
1406
+		client.alwaysOn = false
1407
+	}
1393 1408
 
1394 1409
 	var remainingSessions int
1395 1410
 	if session == nil {
@@ -1459,7 +1474,6 @@ func (client *Client) destroy(session *Session) {
1459 1474
 	}
1460 1475
 
1461 1476
 	// destroy all applicable sessions:
1462
-	var quitMessage string
1463 1477
 	for _, session := range sessionsToDestroy {
1464 1478
 		if session.client != client {
1465 1479
 			// session has been attached to a new client; do not destroy it
@@ -1468,7 +1482,7 @@ func (client *Client) destroy(session *Session) {
1468 1482
 		session.stopIdleTimer()
1469 1483
 		// send quit/error message to client if they haven't been sent already
1470 1484
 		client.Quit("", session)
1471
-		quitMessage = session.quitMessage
1485
+		quitMessage = session.quitMessage // doesn't need synch, we already detached
1472 1486
 		session.SetDestroyed()
1473 1487
 		session.socket.Close()
1474 1488
 
@@ -1506,13 +1520,7 @@ func (client *Client) destroy(session *Session) {
1506 1520
 		return
1507 1521
 	}
1508 1522
 
1509
-	splitQuitMessage := utils.MakeMessage(quitMessage)
1510
-	quitItem := history.Item{
1511
-		Type:        history.Quit,
1512
-		Nick:        details.nickMask,
1513
-		AccountName: details.accountName,
1514
-		Message:     splitQuitMessage,
1515
-	}
1523
+	var quitItem history.Item
1516 1524
 	var channels []*Channel
1517 1525
 	// use a defer here to avoid writing to mysql while holding the destroy semaphore:
1518 1526
 	defer func() {
@@ -1574,6 +1582,13 @@ func (client *Client) destroy(session *Session) {
1574 1582
 	if quitMessage == "" {
1575 1583
 		quitMessage = "Exited"
1576 1584
 	}
1585
+	splitQuitMessage := utils.MakeMessage(quitMessage)
1586
+	quitItem = history.Item{
1587
+		Type:        history.Quit,
1588
+		Nick:        details.nickMask,
1589
+		AccountName: details.accountName,
1590
+		Message:     splitQuitMessage,
1591
+	}
1577 1592
 	var cache MessageCache
1578 1593
 	cache.Initialize(client.server, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
1579 1594
 	for friend := range friends {

+ 5
- 4
irc/config.go Ver fichero

@@ -223,10 +223,11 @@ func historyEnabled(serverSetting PersistentStatus, localSetting HistoryStatus)
223 223
 }
224 224
 
225 225
 type MulticlientConfig struct {
226
-	Enabled          bool
227
-	AllowedByDefault bool             `yaml:"allowed-by-default"`
228
-	AlwaysOn         PersistentStatus `yaml:"always-on"`
229
-	AutoAway         PersistentStatus `yaml:"auto-away"`
226
+	Enabled            bool
227
+	AllowedByDefault   bool             `yaml:"allowed-by-default"`
228
+	AlwaysOn           PersistentStatus `yaml:"always-on"`
229
+	AutoAway           PersistentStatus `yaml:"auto-away"`
230
+	AlwaysOnExpiration custime.Duration `yaml:"always-on-expiration"`
230 231
 }
231 232
 
232 233
 type throttleConfig struct {

+ 24
- 8
irc/getters.go Ver fichero

@@ -337,26 +337,19 @@ func (client *Client) AccountSettings() (result AccountSettings) {
337 337
 
338 338
 func (client *Client) SetAccountSettings(settings AccountSettings) {
339 339
 	// we mark dirty if the client is transitioning to always-on
340
-	var becameAlwaysOn, autoreplayMissedDisabled bool
340
+	var becameAlwaysOn bool
341 341
 	alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
342 342
 	client.stateMutex.Lock()
343 343
 	if client.registered {
344 344
 		// only allow the client to become always-on if their nick equals their account name
345 345
 		alwaysOn = alwaysOn && client.nick == client.accountName
346
-		autoreplayMissedDisabled = (client.accountSettings.AutoreplayMissed && !settings.AutoreplayMissed)
347 346
 		becameAlwaysOn = (!client.alwaysOn && alwaysOn)
348 347
 		client.alwaysOn = alwaysOn
349
-		if autoreplayMissedDisabled {
350
-			// clear the lastSeen entry for the default session, but not for device IDs
351
-			delete(client.lastSeen, "")
352
-		}
353 348
 	}
354 349
 	client.accountSettings = settings
355 350
 	client.stateMutex.Unlock()
356 351
 	if becameAlwaysOn {
357 352
 		client.markDirty(IncludeAllAttrs)
358
-	} else if autoreplayMissedDisabled {
359
-		client.markDirty(IncludeLastSeen)
360 353
 	}
361 354
 }
362 355
 
@@ -449,6 +442,29 @@ func (client *Client) Realname() string {
449 442
 	return result
450 443
 }
451 444
 
445
+func (client *Client) IsExpiredAlwaysOn(config *Config) (result bool) {
446
+	client.stateMutex.Lock()
447
+	defer client.stateMutex.Unlock()
448
+	return client.checkAlwaysOnExpirationNoMutex(config)
449
+}
450
+
451
+func (client *Client) checkAlwaysOnExpirationNoMutex(config *Config) (result bool) {
452
+	if !(client.registered && client.alwaysOn) {
453
+		return false
454
+	}
455
+	deadline := time.Duration(config.Accounts.Multiclient.AlwaysOnExpiration)
456
+	if deadline == 0 {
457
+		return false
458
+	}
459
+	now := time.Now()
460
+	for _, ts := range client.lastSeen {
461
+		if now.Sub(ts) < deadline {
462
+			return false
463
+		}
464
+	}
465
+	return true
466
+}
467
+
452 468
 func (channel *Channel) Name() string {
453 469
 	channel.stateMutex.RLock()
454 470
 	defer channel.stateMutex.RUnlock()

+ 32
- 0
irc/server.go Ver fichero

@@ -12,6 +12,7 @@ import (
12 12
 	_ "net/http/pprof"
13 13
 	"os"
14 14
 	"os/signal"
15
+	"runtime/debug"
15 16
 	"strconv"
16 17
 	"strings"
17 18
 	"sync"
@@ -33,6 +34,10 @@ import (
33 34
 	"github.com/tidwall/buntdb"
34 35
 )
35 36
 
37
+const (
38
+	alwaysOnExpirationPollPeriod = time.Hour
39
+)
40
+
36 41
 var (
37 42
 	// common error line to sub values into
38 43
 	errorMsg = "ERROR :%s\r\n"
@@ -114,6 +119,8 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
114 119
 	signal.Notify(server.signals, ServerExitSignals...)
115 120
 	signal.Notify(server.rehashSignal, syscall.SIGHUP)
116 121
 
122
+	time.AfterFunc(alwaysOnExpirationPollPeriod, server.handleAlwaysOnExpirations)
123
+
117 124
 	return server, nil
118 125
 }
119 126
 
@@ -227,6 +234,31 @@ func (server *Server) checkTorLimits() (banned bool, message string) {
227 234
 	}
228 235
 }
229 236
 
237
+func (server *Server) handleAlwaysOnExpirations() {
238
+	defer func() {
239
+		if r := recover(); r != nil {
240
+			server.logger.Error("internal",
241
+				fmt.Sprintf("Panic in always-on cleanup: %v\n%s", r, debug.Stack()))
242
+		}
243
+		// either way, reschedule
244
+		time.AfterFunc(alwaysOnExpirationPollPeriod, server.handleAlwaysOnExpirations)
245
+	}()
246
+
247
+	config := server.Config()
248
+	deadline := time.Duration(config.Accounts.Multiclient.AlwaysOnExpiration)
249
+	if deadline == 0 {
250
+		return
251
+	}
252
+	server.logger.Info("accounts", "Checking always-on clients for expiration")
253
+	for _, client := range server.clients.AllClients() {
254
+		if client.IsExpiredAlwaysOn(config) {
255
+			// TODO save the channels list, use it for autojoin if/when they return?
256
+			server.logger.Info("accounts", "Expiring always-on client", client.AccountName())
257
+			client.destroy(nil)
258
+		}
259
+	}
260
+}
261
+
230 262
 //
231 263
 // server functionality
232 264
 //

+ 4
- 0
traditional.yaml Ver fichero

@@ -472,6 +472,10 @@ accounts:
472 472
         # whether to mark always-on clients away when they have no active connections:
473 473
         auto-away: "opt-in"
474 474
 
475
+        # QUIT always-on clients from the server if they go this long without connecting
476
+        # (use 0 or omit for no expiration):
477
+        #always-on-expiration: 90d
478
+
475 479
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
476 480
     # hostname/IP) by the HostServ service
477 481
     vhosts:

Loading…
Cancelar
Guardar