ソースを参照

Implement expiration for always-on clients

Fixes #810
tags/v2.5.0-rc1
Shivaram Lingamneni 3年前
コミット
48166b5b4b
6個のファイルの変更101行の追加29行の削除
  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 ファイルの表示

500
         # whether to mark always-on clients away when they have no active connections:
500
         # whether to mark always-on clients away when they have no active connections:
501
         auto-away: "opt-in"
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
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
507
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
504
     # hostname/IP) by the HostServ service
508
     # hostname/IP) by the HostServ service
505
     vhosts:
509
     vhosts:

+ 32
- 17
irc/client.go ファイルの表示

237
 }
237
 }
238
 
238
 
239
 // sets the session quit message, if there isn't one already
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
 	if message == "" {
241
 	if message == "" {
242
 		message = "Connection closed"
242
 		message = "Connection closed"
243
 	}
243
 	}
443
 		nextSessionID: 1,
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
 	client.SetMode(modes.TLS, true)
451
 	client.SetMode(modes.TLS, true)
447
 	for _, m := range uModes {
452
 	for _, m := range uModes {
448
 		client.SetMode(m, true)
453
 		client.SetMode(m, true)
789
 	var markDirty bool
794
 	var markDirty bool
790
 	now := time.Now().UTC()
795
 	now := time.Now().UTC()
791
 	client.stateMutex.Lock()
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
 	client.stateMutex.Unlock()
807
 	client.stateMutex.Unlock()
801
 	if markDirty {
808
 	if markDirty {
802
 		client.markDirty(IncludeLastSeen)
809
 		client.markDirty(IncludeLastSeen)
1364
 	}
1371
 	}
1365
 
1372
 
1366
 	for _, session := range sessions {
1373
 	for _, session := range sessions {
1367
-		if session.SetQuitMessage(message) {
1374
+		if session.setQuitMessage(message) {
1368
 			setFinalData(session)
1375
 			setFinalData(session)
1369
 		}
1376
 		}
1370
 	}
1377
 	}
1378
 	config := client.server.Config()
1385
 	config := client.server.Config()
1379
 	var sessionsToDestroy []*Session
1386
 	var sessionsToDestroy []*Session
1380
 	var saveLastSeen bool
1387
 	var saveLastSeen bool
1388
+	var quitMessage string
1381
 
1389
 
1382
 	client.stateMutex.Lock()
1390
 	client.stateMutex.Lock()
1383
 
1391
 
1390
 	// XXX a temporary (reattaching) client can be marked alwaysOn when it logs in,
1398
 	// XXX a temporary (reattaching) client can be marked alwaysOn when it logs in,
1391
 	// but then the session attaches to another client and we need to clean it up here
1399
 	// but then the session attaches to another client and we need to clean it up here
1392
 	alwaysOn := registered && client.alwaysOn
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
 	var remainingSessions int
1409
 	var remainingSessions int
1395
 	if session == nil {
1410
 	if session == nil {
1459
 	}
1474
 	}
1460
 
1475
 
1461
 	// destroy all applicable sessions:
1476
 	// destroy all applicable sessions:
1462
-	var quitMessage string
1463
 	for _, session := range sessionsToDestroy {
1477
 	for _, session := range sessionsToDestroy {
1464
 		if session.client != client {
1478
 		if session.client != client {
1465
 			// session has been attached to a new client; do not destroy it
1479
 			// session has been attached to a new client; do not destroy it
1468
 		session.stopIdleTimer()
1482
 		session.stopIdleTimer()
1469
 		// send quit/error message to client if they haven't been sent already
1483
 		// send quit/error message to client if they haven't been sent already
1470
 		client.Quit("", session)
1484
 		client.Quit("", session)
1471
-		quitMessage = session.quitMessage
1485
+		quitMessage = session.quitMessage // doesn't need synch, we already detached
1472
 		session.SetDestroyed()
1486
 		session.SetDestroyed()
1473
 		session.socket.Close()
1487
 		session.socket.Close()
1474
 
1488
 
1506
 		return
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
 	var channels []*Channel
1524
 	var channels []*Channel
1517
 	// use a defer here to avoid writing to mysql while holding the destroy semaphore:
1525
 	// use a defer here to avoid writing to mysql while holding the destroy semaphore:
1518
 	defer func() {
1526
 	defer func() {
1574
 	if quitMessage == "" {
1582
 	if quitMessage == "" {
1575
 		quitMessage = "Exited"
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
 	var cache MessageCache
1592
 	var cache MessageCache
1578
 	cache.Initialize(client.server, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
1593
 	cache.Initialize(client.server, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
1579
 	for friend := range friends {
1594
 	for friend := range friends {

+ 5
- 4
irc/config.go ファイルの表示

223
 }
223
 }
224
 
224
 
225
 type MulticlientConfig struct {
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
 type throttleConfig struct {
233
 type throttleConfig struct {

+ 24
- 8
irc/getters.go ファイルの表示

337
 
337
 
338
 func (client *Client) SetAccountSettings(settings AccountSettings) {
338
 func (client *Client) SetAccountSettings(settings AccountSettings) {
339
 	// we mark dirty if the client is transitioning to always-on
339
 	// we mark dirty if the client is transitioning to always-on
340
-	var becameAlwaysOn, autoreplayMissedDisabled bool
340
+	var becameAlwaysOn bool
341
 	alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
341
 	alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
342
 	client.stateMutex.Lock()
342
 	client.stateMutex.Lock()
343
 	if client.registered {
343
 	if client.registered {
344
 		// only allow the client to become always-on if their nick equals their account name
344
 		// only allow the client to become always-on if their nick equals their account name
345
 		alwaysOn = alwaysOn && client.nick == client.accountName
345
 		alwaysOn = alwaysOn && client.nick == client.accountName
346
-		autoreplayMissedDisabled = (client.accountSettings.AutoreplayMissed && !settings.AutoreplayMissed)
347
 		becameAlwaysOn = (!client.alwaysOn && alwaysOn)
346
 		becameAlwaysOn = (!client.alwaysOn && alwaysOn)
348
 		client.alwaysOn = alwaysOn
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
 	client.accountSettings = settings
349
 	client.accountSettings = settings
355
 	client.stateMutex.Unlock()
350
 	client.stateMutex.Unlock()
356
 	if becameAlwaysOn {
351
 	if becameAlwaysOn {
357
 		client.markDirty(IncludeAllAttrs)
352
 		client.markDirty(IncludeAllAttrs)
358
-	} else if autoreplayMissedDisabled {
359
-		client.markDirty(IncludeLastSeen)
360
 	}
353
 	}
361
 }
354
 }
362
 
355
 
449
 	return result
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
 func (channel *Channel) Name() string {
468
 func (channel *Channel) Name() string {
453
 	channel.stateMutex.RLock()
469
 	channel.stateMutex.RLock()
454
 	defer channel.stateMutex.RUnlock()
470
 	defer channel.stateMutex.RUnlock()

+ 32
- 0
irc/server.go ファイルの表示

12
 	_ "net/http/pprof"
12
 	_ "net/http/pprof"
13
 	"os"
13
 	"os"
14
 	"os/signal"
14
 	"os/signal"
15
+	"runtime/debug"
15
 	"strconv"
16
 	"strconv"
16
 	"strings"
17
 	"strings"
17
 	"sync"
18
 	"sync"
33
 	"github.com/tidwall/buntdb"
34
 	"github.com/tidwall/buntdb"
34
 )
35
 )
35
 
36
 
37
+const (
38
+	alwaysOnExpirationPollPeriod = time.Hour
39
+)
40
+
36
 var (
41
 var (
37
 	// common error line to sub values into
42
 	// common error line to sub values into
38
 	errorMsg = "ERROR :%s\r\n"
43
 	errorMsg = "ERROR :%s\r\n"
114
 	signal.Notify(server.signals, ServerExitSignals...)
119
 	signal.Notify(server.signals, ServerExitSignals...)
115
 	signal.Notify(server.rehashSignal, syscall.SIGHUP)
120
 	signal.Notify(server.rehashSignal, syscall.SIGHUP)
116
 
121
 
122
+	time.AfterFunc(alwaysOnExpirationPollPeriod, server.handleAlwaysOnExpirations)
123
+
117
 	return server, nil
124
 	return server, nil
118
 }
125
 }
119
 
126
 
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
 // server functionality
263
 // server functionality
232
 //
264
 //

+ 4
- 0
traditional.yaml ファイルの表示

472
         # whether to mark always-on clients away when they have no active connections:
472
         # whether to mark always-on clients away when they have no active connections:
473
         auto-away: "opt-in"
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
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
479
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
476
     # hostname/IP) by the HostServ service
480
     # hostname/IP) by the HostServ service
477
     vhosts:
481
     vhosts:

読み込み中…
キャンセル
保存