0

Use tomb v2, update deps, fix hang on stale connection

This commit is contained in:
eleksir
2025-01-02 17:28:27 +03:00
parent 189cb99ed9
commit 44004d8fd9
21 changed files with 1282 additions and 520 deletions

View File

@@ -516,7 +516,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
} else { } else {
log.Errorf("Presence notification, Type: %s, From: %s, Show: %s, Status: %s", log.Errorf("Presence notification, Type: %s, From: %s, Show: %s, Status: %s",
v.Type, v.From, v.Show, v.Status) v.Type, v.From, v.Show, v.Status)
log.Errorf(spew.Sdump(v)) log.Error(spew.Sdump(v))
} }
} else { } else {
log.Infof( log.Infof(

View File

@@ -7,7 +7,7 @@ import (
"github.com/cockroachdb/pebble" "github.com/cockroachdb/pebble"
"github.com/eleksir/go-xmpp" "github.com/eleksir/go-xmpp"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"gopkg.in/tomb.v1" "gopkg.in/tomb.v2"
) )
// Config - это у нас глобальная штука :) . // Config - это у нас глобальная штука :) .
@@ -33,7 +33,7 @@ var connecting = false
var isConnected = false var isConnected = false
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов. // Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
var sigChan = make(chan os.Signal, 1) var sigChan chan os.Signal
// Основной инстанс xmpp-клиента. // Основной инстанс xmpp-клиента.
var talk *xmpp.Client var talk *xmpp.Client

3
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/hjson/hjson-go v3.3.0+incompatible github.com/hjson/hjson-go v3.3.0+incompatible
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
) )
require ( require (
@@ -37,6 +37,7 @@ require (
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect
golang.org/x/crypto v0.25.0 // indirect golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect

2
go.sum
View File

@@ -127,6 +127,8 @@ google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWn
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

332
lib.go
View File

@@ -16,7 +16,7 @@ import (
) )
// sigHandler Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения. // sigHandler Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
func sigHandler() { func sigHandler() error {
log.Debug("Installing signal handler") log.Debug("Installing signal handler")
for s := range sigChan { for s := range sigChan {
@@ -86,14 +86,16 @@ func sigHandler() {
os.Exit(0) os.Exit(0)
} }
return nil
} }
// establishConnection устанавливает соединение с jabber-сервером. // establishConnection устанавливает соединение с jabber-сервером.
func establishConnection() { func establishConnection() error {
var err error var err error
if connecting && !isConnected { if connecting && !isConnected {
return return nil
} }
// Проставляем глобальные переменные. // Проставляем глобальные переменные.
@@ -104,26 +106,22 @@ func establishConnection() {
talk, err = options.NewClient() talk, err = options.NewClient()
if err != nil { if err != nil {
gTomb.Kill(err) return fmt.Errorf("Unable to connect to %s: %w", options.Host, err)
return
} }
// По идее keepalive должен же проходить только, если мы уже на сервере, так? // По идее keepalive должен же проходить только, если мы уже на сервере, так?
if _, err := talk.SendKeepAlive(); err != nil { if _, err := talk.SendKeepAlive(); err != nil {
log.Errorf("Try to send initial KeepAlive, got error: %s", err) return fmt.Errorf("Try to send initial KeepAlive, got error: %w", err)
gTomb.Kill(err)
} }
log.Info("Connected") log.Info("Connected")
// Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили // Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили
for _, room := range config.Jabber.Channels { for _, room := range config.Jabber.Channels {
go joinMuc(room.Name) gTomb.Go(func() error { return joinMuc(room.Name) })
} }
go RotateStatus("") gTomb.Go(func() error { return RotateStatus("") })
lastActivity = time.Now().Unix() lastActivity = time.Now().Unix()
connecting = false connecting = false
@@ -134,21 +132,18 @@ func establishConnection() {
_, err = talk.DiscoverInfo(talk.JID(), config.Jabber.Server) _, err = talk.DiscoverInfo(talk.JID(), config.Jabber.Server)
if err != nil { if err != nil {
log.Infof("Unable to send disco#info to jabber server: %s", err) return fmt.Errorf("Unable to send disco#info to jabber server: %s", err)
gTomb.Kill(err)
return
} }
return nil
} }
// joinMu джойнится к конференциям/каналам/комнатам в джаббере. // joinMu джойнится к конференциям/каналам/комнатам в джаббере.
func joinMuc(room string) { func joinMuc(room string) error {
log.Debugf("Sending disco#info from %s to %s", talk.JID(), room) log.Debugf("Sending disco#info from %s to %s", talk.JID(), room)
if _, err := talk.DiscoverInfo(talk.JID(), room); err != nil { if _, err := talk.DiscoverInfo(talk.JID(), room); err != nil {
log.Infof("Unable to send disco#info to MUC %s: %s", room, err) return fmt.Errorf("Unable to send disco#info to MUC %s: %s", room, err)
gTomb.Kill(err)
} }
// Ждём, пока muc нам вернёт список фичей. // Ждём, пока muc нам вернёт список фичей.
@@ -173,15 +168,13 @@ func joinMuc(room string) {
log.Infof("Unable to join to password-protected room. Don't know how to enter passwords :)") log.Infof("Unable to join to password-protected room. Don't know how to enter passwords :)")
return return nil
} }
} }
// Пытаемся зайти в комнату // Пытаемся зайти в комнату
if _, err := talk.JoinMUCNoHistory(room, getBotNickFromRoomConfig(room)); err != nil { if _, err := talk.JoinMUCNoHistory(room, getBotNickFromRoomConfig(room)); err != nil {
log.Errorf("Unable to join to MUC: %s", room) return fmt.Errorf("Unable to join to MUC: %s, %w", room, err)
gTomb.Kill(err)
} }
log.Infof("Joining to MUC: %s", room) log.Infof("Joining to MUC: %s", room)
@@ -206,172 +199,152 @@ func joinMuc(room string) {
20*int(config.Jabber.ConnectionTimeout)+1, 20*int(config.Jabber.ConnectionTimeout)+1,
) )
return return nil
} }
// Вот теперь точно можно слать статус. // Вот теперь точно можно слать статус.
log.Infof("Joined to MUC: %s", room) log.Infof("Joined to MUC: %s", room)
go RotateStatus(room) gTomb.Go(func() error { return RotateStatus(room) })
return nil
} }
// probeServerLiveness проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой // probeServerLiveness проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой
// они выкидывают клиента через некоторое время неактивности. // они выкидывают клиента через некоторое время неактивности.
func probeServerLiveness() { //nolint:gocognit func probeServerLiveness() error { //nolint:gocognit
for { for {
select { for {
case <-gTomb.Dying(): if shutdown {
gTomb.Done() return nil
return }
default: sleepTime := time.Duration(config.Jabber.ServerPingDelay) * 1000 * time.Millisecond
for { sleepTime += time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond
if shutdown { time.Sleep(sleepTime)
return
}
sleepTime := time.Duration(config.Jabber.ServerPingDelay) * 1000 * time.Millisecond if !isConnected {
sleepTime += time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond continue
time.Sleep(sleepTime) }
if !isConnected { // Пингуем, только если не было никакой активности в течение > config.Jabber.ServerPingDelay,
continue // в худшем случе это будет ~ (config.Jabber.PingSplayDelay * 2) + config.Jabber.PingSplayDelay
} // if (time.Now().Unix() - lastServerActivity) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
// continue
// }
// Пингуем, только если не было никакой активности в течение > config.Jabber.ServerPingDelay, if serverCapsQueried { // Сервер ответил на disco#info
// в худшем случе это будет ~ (config.Jabber.PingSplayDelay * 2) + config.Jabber.PingSplayDelay var (
// if (time.Now().Unix() - lastServerActivity) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) { value interface{}
// continue exist bool
// } )
if serverCapsQueried { // Сервер ответил на disco#info value, exist = serverCapsList.Get("urn:xmpp:ping")
var (
value interface{}
exist bool
)
value, exist = serverCapsList.Get("urn:xmpp:ping") switch {
// Сервер анонсировал, что умеет в c2s пинги
case exist && value.(bool):
// Таймаут c2s пинга. Возьмём сумму задержки между пингами, добавим таймаут коннекта и добавим
// максимальную корректировку разброса.
txTimeout := config.Jabber.ServerPingDelay + config.Jabber.ConnectionTimeout
txTimeout += config.Jabber.PingSplayDelay
rxTimeout := txTimeout
switch { rxTimeAgo := time.Now().Unix() - serverPingTimestampRx
// Сервер анонсировал, что умеет в c2s пинги
case exist && value.(bool):
// Таймаут c2s пинга. Возьмём сумму задержки между пингами, добавим таймаут коннекта и добавим
// максимальную корректировку разброса.
txTimeout := config.Jabber.ServerPingDelay + config.Jabber.ConnectionTimeout
txTimeout += config.Jabber.PingSplayDelay
rxTimeout := txTimeout
rxTimeAgo := time.Now().Unix() - serverPingTimestampRx if serverPingTimestampTx > 0 { // Первая пуля от нас ушла...
switch {
// Давненько мы не получали понгов от сервера, вероятно, соединение с сервером утеряно?
case rxTimeAgo > (rxTimeout * 2):
err := fmt.Errorf( //nolint:goerr113
"stall connection detected. No c2s pong for %d seconds",
rxTimeAgo,
)
if serverPingTimestampTx > 0 { // Первая пуля от нас ушла... return err
switch {
// Давненько мы не получали понгов от сервера, вероятно, соединение с сервером утеряно?
case rxTimeAgo > (rxTimeout * 2):
err := fmt.Errorf( //nolint:goerr113
"stall connection detected. No c2s pong for %d seconds",
rxTimeAgo,
)
gTomb.Kill(err) // По-умолчанию, мы отправляем c2s пинг
default:
continue log.Debugf("Sending c2s ping from %s to %s", talk.JID(), config.Jabber.Server)
// По-умолчанию, мы отправляем c2s пинг
default:
log.Debugf("Sending c2s ping from %s to %s", talk.JID(), config.Jabber.Server)
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
gTomb.Kill(err)
continue
}
serverPingTimestampTx = time.Now().Unix()
}
} else { // Первая пуля пока не вылетела, отправляем
log.Debugf("Sending first c2s ping from %s to %s", talk.JID(), config.Jabber.Server)
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil { if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
gTomb.Kill(err) return err
continue
} }
serverPingTimestampTx = time.Now().Unix() serverPingTimestampTx = time.Now().Unix()
} }
} else { // Первая пуля пока не вылетела, отправляем
log.Debugf("Sending first c2s ping from %s to %s", talk.JID(), config.Jabber.Server)
// Сервер не анонсировал, что умеет в c2s пинги if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
default: return err
log.Debug("Sending keepalive whitespace ping")
if _, err := talk.SendKeepAlive(); err != nil {
gTomb.Kill(err)
continue
} }
serverPingTimestampTx = time.Now().Unix()
} }
} else { // Сервер не ответил на disco#info
// Сервер не анонсировал, что умеет в c2s пинги
default:
log.Debug("Sending keepalive whitespace ping") log.Debug("Sending keepalive whitespace ping")
if _, err := talk.SendKeepAlive(); err != nil { if _, err := talk.SendKeepAlive(); err != nil {
gTomb.Kill(err) return err
continue
} }
} }
} else { // Сервер не ответил на disco#info
log.Debug("Sending keepalive whitespace ping")
if _, err := talk.SendKeepAlive(); err != nil {
return err
}
} }
} }
} }
} }
// probeMUCLiveness Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е. // probeMUCLiveness Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е.
func probeMUCLiveness() { //nolint:gocognit func probeMUCLiveness() error { //nolint:gocognit
for { for {
select { for {
case <-gTomb.Dying(): for _, room := range roomsConnected {
gTomb.Done() var (
return exist bool
lastActivityTS interface{}
)
default: // Если записи про комнату нету, то пинговать её бессмысленно.
for { if lastActivityTS, exist = lastMucActivity.Get(room); !exist {
for _, room := range roomsConnected { continue
var ( }
exist bool
lastActivityTS interface{}
)
// Если записи про комнату нету, то пинговать её бессмысленно. // Если время последней активности в чятике не превысило
if lastActivityTS, exist = lastMucActivity.Get(room); !exist { // config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay, ничего не пингуем.
if (time.Now().Unix() - lastActivityTS.(int64)) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
continue
}
/* Пинг MUC-а по сценарию без серверной оптимизации мы реализовывать не будем. Это как-то не надёжно.
go func(room string) {
// Небольшая рандомная задержка перед пингом комнаты
sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
time.Sleep(sleepTime)
if err := talk.PingS2S(talk.JID(), room+"/"+getBotNickFromRoomConfig(room)); err != nil {
gTomb.Kill(err)
continue continue
} }
}(room)
*/
// Если время последней активности в чятике не превысило var roomMap interface{}
// config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay, ничего не пингуем.
if (time.Now().Unix() - lastActivityTS.(int64)) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
continue
}
/* Пинг MUC-а по сценарию без серверной оптимизации мы реализовывать не будем. Это как-то не надёжно. roomMap, exist = mucCapsList.Get(room)
go func(room string) {
// Небольшая рандомная задержка перед пингом комнаты
sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
time.Sleep(sleepTime)
if err := talk.PingS2S(talk.JID(), room+"/"+getBotNickFromRoomConfig(room)); err != nil { // Пинги комнаты проводим, только если она записана, как прошедшая disco#info и поддерживающая
gTomb.Kill(err) // Server Optimization.
continue if exist && roomMap.(map[string]bool)["http://jabber.org/protocol/muc#self-ping-optimization"] {
} gTomb.Go(
}(room) func() error {
*/
var roomMap interface{}
roomMap, exist = mucCapsList.Get(room)
// Пинги комнаты проводим, только если она записана, как прошедшая disco#info и поддерживающая
// Server Optimization.
if exist && roomMap.(map[string]bool)["http://jabber.org/protocol/muc#self-ping-optimization"] {
go func(room string) {
// Небольшая рандомная задержка перед пингом комнаты. // Небольшая рандомная задержка перед пингом комнаты.
sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond
time.Sleep(sleepTime) time.Sleep(sleepTime)
@@ -379,65 +352,54 @@ func probeMUCLiveness() { //nolint:gocognit
log.Debugf("Sending MUC ping from %s to %s", talk.JID(), room) log.Debugf("Sending MUC ping from %s to %s", talk.JID(), room)
if err := talk.PingS2S(talk.JID(), room); err != nil { if err := talk.PingS2S(talk.JID(), room); err != nil {
err := fmt.Errorf("unable to ping MUC %s: %w", room, err) return fmt.Errorf("unable to ping MUC %s: %w", room, err)
gTomb.Kill(err)
} }
}(room)
}
}
time.Sleep(time.Duration(config.Jabber.MucPingDelay) * time.Second) return nil
},
)
}
} }
time.Sleep(time.Duration(config.Jabber.MucPingDelay) * time.Second)
} }
} }
} }
// RotateStatus периодически изменяет статус бота в MUC-е согласно настройкам из кофига. // RotateStatus периодически изменяет статус бота в MUC-е согласно настройкам из кофига.
func RotateStatus(room string) { func RotateStatus(room string) error {
for { for {
select { // TODO: Переделать на ticker-ы
case <-gTomb.Dying(): totalSleepTime := time.Duration(config.Jabber.RuntimeStatus.RotationTime) * time.Second
gTomb.Done() totalSleepTime += time.Duration(config.Jabber.RuntimeStatus.RotationSplayTime) * time.Second
return
default: for {
// TODO: Переделать на ticker-ы status := randomPhrase(config.Jabber.RuntimeStatus.Text)
totalSleepTime := time.Duration(config.Jabber.RuntimeStatus.RotationTime) * time.Second log.Debugf("Set status for MUC: %s to: %s", room, status)
totalSleepTime += time.Duration(config.Jabber.RuntimeStatus.RotationSplayTime) * time.Second
for { var p xmpp.Presence
status := randomPhrase(config.Jabber.RuntimeStatus.Text)
log.Debugf("Set status for MUC: %s to: %s", room, status)
var p xmpp.Presence if room != "" {
p = xmpp.Presence{ //nolint:exhaustruct
if room != "" { To: room,
p = xmpp.Presence{ //nolint:exhaustruct Status: status,
To: room,
Status: status,
}
} else {
p = xmpp.Presence{ //nolint:exhaustruct
Status: status,
}
} }
} else {
if _, err := talk.SendPresence(p); err != nil { p = xmpp.Presence{ //nolint:exhaustruct
log.Infof("Unable to send presence to MUC %s: %s", room, err) Status: status,
gTomb.Kill(err)
return
} }
// Если мы не хотим ротировать, то цикл нам тут не нужен, просто выходим.
if config.Jabber.RuntimeStatus.RotationTime == 0 {
gTomb.Done()
return
}
time.Sleep(totalSleepTime)
} }
if _, err := talk.SendPresence(p); err != nil {
return fmt.Errorf("Unable to send presence to MUC %s: %s", room, err)
}
// Если мы не хотим ротировать, то цикл нам тут не нужен, просто выходим.
if config.Jabber.RuntimeStatus.RotationTime == 0 {
return nil
}
time.Sleep(totalSleepTime)
} }
} }
} }

277
main.go
View File

@@ -2,10 +2,7 @@ package main
import ( import (
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"io"
"net"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
@@ -14,10 +11,10 @@ import (
"github.com/eleksir/go-xmpp" "github.com/eleksir/go-xmpp"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/tomb.v1" "gopkg.in/tomb.v2"
) )
func init() { func main() {
log.SetFormatter(&log.TextFormatter{ //nolint:exhaustruct log.SetFormatter(&log.TextFormatter{ //nolint:exhaustruct
DisableQuote: true, DisableQuote: true,
DisableLevelTruncation: false, DisableLevelTruncation: false,
@@ -35,73 +32,78 @@ func init() {
os.Exit(1) os.Exit(1)
} }
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath)) for {
configLocations = append(configLocations, configJSONPath) sigChan = make(chan os.Signal, 1)
talk = new(xmpp.Client)
options = new(xmpp.Options)
if err := readConfig(); err != nil { configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
log.Error(err) configLocations = append(configLocations, configJSONPath)
os.Exit(1) if err := readConfig(); err != nil {
} log.Error(err)
// no panic os.Exit(1)
switch config.Loglevel {
case "fatal":
log.SetLevel(log.FatalLevel)
case "error":
log.SetLevel(log.ErrorLevel)
case "warn":
log.SetLevel(log.WarnLevel)
case "info":
log.SetLevel(log.InfoLevel)
case "debug":
log.SetLevel(log.DebugLevel)
case "trace":
log.SetLevel(log.TraceLevel)
default:
log.SetLevel(log.InfoLevel)
}
}
func main() {
// Откроем лог и скормим его логгеру
if config.Log != "" {
logfile, err := os.OpenFile(config.Log, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
log.Fatalf("Unable to open log file %s: %s", config.Log, err)
} }
log.SetOutput(logfile) // no panic
} switch config.Loglevel {
case "fatal":
log.SetLevel(log.FatalLevel)
case "error":
log.SetLevel(log.ErrorLevel)
case "warn":
log.SetLevel(log.WarnLevel)
case "info":
log.SetLevel(log.InfoLevel)
case "debug":
log.SetLevel(log.DebugLevel)
case "trace":
log.SetLevel(log.TraceLevel)
default:
log.SetLevel(log.InfoLevel)
}
myLogLevel := log.GetLevel() // Откроем лог и скормим его логгеру
log.Warnf("Loglevel set to %v", myLogLevel) if config.Log != "" {
logfile, err := os.OpenFile(config.Log, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
verboseClient := false if err != nil {
log.Fatalf("Unable to open log file %s: %s", config.Log, err)
}
if myLogLevel == log.TraceLevel { log.SetOutput(logfile)
verboseClient = true }
}
// github.com/mattn/go-xmpp пишет в stdio, нам этого не надо, ловим выхлоп его в logrus с уровнем trace. myLogLevel := log.GetLevel()
xmpp.DebugWriter = log.WithFields(log.Fields{"logger": "stdlib"}).WriterLevel(log.TraceLevel) log.Warnf("Loglevel set to %v", myLogLevel)
// Хэндлер сигналов не надо трогать, он нужен для завершения программы целиком. // Через tomb попробуем сделать выход горутинок управляемым.
go sigHandler() gTomb := tomb.Tomb{}
signal.Notify(sigChan, os.Interrupt)
// Иницализируем redis-клиента. // Иницализируем redis-клиента.
redisClient = redis.NewClient(&redis.Options{ //nolint:exhaustruct redisClient = redis.NewClient(&redis.Options{ //nolint:exhaustruct
Addr: fmt.Sprintf("%s:%d", config.Redis.Server, config.Redis.Port), Addr: fmt.Sprintf("%s:%d", config.Redis.Server, config.Redis.Port),
}) })
log.Debugf("Lazy connect() to redis at %s:%d", config.Redis.Server, config.Redis.Port) log.Debugf("Lazy connect() to redis at %s:%d", config.Redis.Server, config.Redis.Port)
subscriber = redisClient.Subscribe(ctx, config.Redis.MyChannel) subscriber = redisClient.Subscribe(ctx, config.Redis.MyChannel)
go redisLoop(subscriber.Channel()) verboseClient := false
if myLogLevel == log.TraceLevel {
verboseClient = true
}
// github.com/mattn/go-xmpp пишет в stdio, нам этого не надо, ловим выхлоп его в logrus с уровнем trace.
xmpp.DebugWriter = log.WithFields(log.Fields{"logger": "stdlib"}).WriterLevel(log.TraceLevel)
// Хэндлер сигналов не надо трогать, он нужен для завершения программы целиком.
gTomb.Go(func() error { return sigHandler() })
signal.Notify(sigChan, os.Interrupt)
gTomb.Go(func() error { return redisLoop(subscriber.Channel()) })
for {
options = &xmpp.Options{ //nolint:exhaustruct options = &xmpp.Options{ //nolint:exhaustruct
Host: fmt.Sprintf("%s:%d", config.Jabber.Server, config.Jabber.Port), Host: fmt.Sprintf("%s:%d", config.Jabber.Server, config.Jabber.Port),
User: config.Jabber.User, User: config.Jabber.User,
@@ -121,109 +123,84 @@ func main() {
DialTimeout: time.Duration(config.Jabber.ConnectionTimeout) * time.Second, DialTimeout: time.Duration(config.Jabber.ConnectionTimeout) * time.Second,
} }
// Через tomb попробуем сделать выход горутинок управляемым.
gTomb = tomb.Tomb{}
// Устанавливаем соединение и гребём события, посылаемые сервером. // Устанавливаем соединение и гребём события, посылаемые сервером.
myLoop() gTomb.Go(func() error { return myLoop() })
log.Error(gTomb.Wait()) // Ловим первый же kill и не дождаемся остальных, хотя формально надо бы.
_ = <-gTomb.Dying()
// Если у нас wire error, то вызов .Close() повлечёт за собой ошибку, но мы вынуждены звать .Close(), чтоб
// закрыть tls контекст и почистить всё что связанно с прерванным соединением.
log.Infoln("Closing connection to jabber server")
_ = talk.Close()
time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second) time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second)
} }
} }
func myLoop() { func myLoop() error {
for { for {
select { // Зададим начальное значение глобальным переменным.
case <-gTomb.Dying(): serverPingTimestampRx = 0
gTomb.Done() serverPingTimestampTx = 0
roomsConnected = make([]string, 1)
lastActivity = 0
lastServerActivity = 0
lastMucActivity = NewCollection()
serverCapsQueried = false
serverCapsList = NewCollection()
mucCapsList = NewCollection()
serverPingTimestampTx = 0
serverPingTimestampRx = 0
roomPresences = NewCollection()
return // Установим коннект.
if err := establishConnection(); err != nil {
log.Error(err)
default: return err
// Зададим начальное значение глобальным переменным.
serverPingTimestampRx = 0
serverPingTimestampTx = 0
roomsConnected = make([]string, 1)
lastActivity = 0
lastServerActivity = 0
lastMucActivity = NewCollection()
serverCapsQueried = false
serverCapsList = NewCollection()
mucCapsList = NewCollection()
serverPingTimestampTx = 0
serverPingTimestampRx = 0
roomPresences = NewCollection()
// Установим коннект.
establishConnection()
serverPingTimestampRx = time.Now().Unix() // Считаем, что если коннект запустился, то первый пинг успешен.
// Тыкаем сервер палочкой, проверяем, что коннект жив и переустанавливаем его, если он не жив.
go probeServerLiveness()
// Тыкаем muc-и палочкой, проверяем, что они живы и пере-заходим в них, если пинги пропали.
go probeMUCLiveness()
// Гребём сообщения...
for {
// Стриггерилось завершение работы приложения, или соединение не установлено (порвалось, например).
// грести не надо.
if shutdown {
break
}
if !isConnected {
// Tight loop - это наверно не очень хорошо, думаю, ничего страшного не будет, если мы поспим 100мс.
time.Sleep(100 * time.Millisecond)
continue
}
// Вынимаем ивент из "провода".
chat, err := talk.Recv()
if err != nil {
log.Errorf("Unable to get events from server: %s", err)
// Попробуем скрасить печальные будни generic-ошибок...
switch {
// Стрим не читается, он закрылся с той стороны во время чтения.
case errors.Is(err, io.EOF):
err = fmt.Errorf("tcp stream closed while reading, err=%w", err)
// Пытаемся читать закрытый сокет.
case errors.Is(err, net.ErrClosed):
err = fmt.Errorf("unable to read closed socket, err=%w", err)
// Не смогли записать в сокет.
case errors.Is(err, net.ErrWriteToConnected):
err = fmt.Errorf("unable to write to socket, err=%w", err)
// Не сетевая проблема.
default:
// Это уже что-то странное.
// Вероятно, ошибка парсинга xml. Собственно, баг сервера, тут мы ничего поделать не можем.
err = fmt.Errorf("error during parsing received message, err=%w", err)
}
gTomb.Kill(err)
// Не забываем выходить на ошибке :(
return
}
parseEvent(chat)
}
} }
serverPingTimestampRx = time.Now().Unix() // Считаем, что если коннект запустился, то первый пинг успешен.
// Тыкаем сервер палочкой, проверяем, что коннект жив и вываливаемся из mainLoop, если он не жив.
gTomb.Go(
func() error {
err := probeServerLiveness()
log.Error(err)
return err
},
)
// Тыкаем muc-и палочкой, проверяем, что они живы и пере-заходим в них, если пинги пропали.
// Если пинги до комнаты пропали, то это фактически значит, что либо сервер потерял связь с MUC-компонентом,
// либо у нас какой-то wire error.
// gTomb.Go() тут зовётся внутри на каждую комнату, если у нас ошибка, эта горутинка рано или поздно выйдет.
go probeMUCLiveness()
// Гребём сообщения...
for {
// Стриггерилось завершение работы приложения, или соединение не установлено (порвалось, например).
// грести не надо.
if shutdown {
break
}
if !isConnected {
// Tight loop - это наверно не очень хорошо, думаю, ничего страшного не будет, если мы поспим 100мс.
time.Sleep(100 * time.Millisecond)
continue
}
// Вынимаем ивент из "провода".
chat, err := talk.Recv()
if err != nil {
log.Errorf("Unable to get recieve data from socket: %s", err)
return err
}
parseEvent(chat)
}
return nil
} }
} }

View File

@@ -9,17 +9,19 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func redisLoop(redisMsgChan <-chan *redis.Message) { func redisLoop(redisMsgChan <-chan *redis.Message) error {
// Обработчик событий от редиски // Обработчик событий от редиски
for msg := range redisMsgChan { for msg := range redisMsgChan {
if shutdown { if shutdown {
return return nil
} }
if err := redisMsgParser(msg.Payload); err != nil { if err := redisMsgParser(msg.Payload); err != nil {
log.Warn(err) log.Warn(err)
} }
} }
return nil
} }
// redisMsgParser парсит json-чики прилетевшие из REDIS-ки. // redisMsgParser парсит json-чики прилетевшие из REDIS-ки.

27
vendor/golang.org/x/net/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

22
vendor/golang.org/x/net/PATENTS generated vendored Normal file
View File

@@ -0,0 +1,22 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.

56
vendor/golang.org/x/net/context/context.go generated vendored Normal file
View File

@@ -0,0 +1,56 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package context defines the Context type, which carries deadlines,
// cancelation signals, and other request-scoped values across API boundaries
// and between processes.
// As of Go 1.7 this package is available in the standard library under the
// name context. https://golang.org/pkg/context.
//
// Incoming requests to a server should create a Context, and outgoing calls to
// servers should accept a Context. The chain of function calls between must
// propagate the Context, optionally replacing it with a modified copy created
// using WithDeadline, WithTimeout, WithCancel, or WithValue.
//
// Programs that use Contexts should follow these rules to keep interfaces
// consistent across packages and enable static analysis tools to check context
// propagation:
//
// Do not store Contexts inside a struct type; instead, pass a Context
// explicitly to each function that needs it. The Context should be the first
// parameter, typically named ctx:
//
// func DoSomething(ctx context.Context, arg Arg) error {
// // ... use ctx ...
// }
//
// Do not pass a nil Context, even if a function permits it. Pass context.TODO
// if you are unsure about which Context to use.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The same Context may be passed to functions running in different goroutines;
// Contexts are safe for simultaneous use by multiple goroutines.
//
// See http://blog.golang.org/context for example code for a server that uses
// Contexts.
package context // import "golang.org/x/net/context"
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter). TODO is recognized by static analysis tools that determine
// whether Contexts are propagated correctly in a program.
func TODO() Context {
return todo
}

72
vendor/golang.org/x/net/context/go17.go generated vendored Normal file
View File

@@ -0,0 +1,72 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.7
package context
import (
"context" // standard library's context, as of Go 1.7
"time"
)
var (
todo = context.TODO()
background = context.Background()
)
// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = context.Canceled
// DeadlineExceeded is the error returned by Context.Err when the context's
// deadline passes.
var DeadlineExceeded = context.DeadlineExceeded
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
ctx, f := context.WithCancel(parent)
return ctx, f
}
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
ctx, f := context.WithDeadline(parent, deadline)
return ctx, f
}
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
func WithValue(parent Context, key interface{}, val interface{}) Context {
return context.WithValue(parent, key, val)
}

20
vendor/golang.org/x/net/context/go19.go generated vendored Normal file
View File

@@ -0,0 +1,20 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.9
package context
import "context" // standard library's context, as of Go 1.7
// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context = context.Context
// A CancelFunc tells an operation to abandon its work.
// A CancelFunc does not wait for the work to stop.
// After the first call, subsequent calls to a CancelFunc do nothing.
type CancelFunc = context.CancelFunc

300
vendor/golang.org/x/net/context/pre_go17.go generated vendored Normal file
View File

@@ -0,0 +1,300 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !go1.7
package context
import (
"errors"
"fmt"
"sync"
"time"
)
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = errors.New("context canceled")
// DeadlineExceeded is the error returned by Context.Err when the context's
// deadline passes.
var DeadlineExceeded = errors.New("context deadline exceeded")
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, c)
return c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) *cancelCtx {
return &cancelCtx{
Context: parent,
done: make(chan struct{}),
}
}
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
return // parent is never canceled
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]bool)
}
p.children[child] = true
}
p.mu.Unlock()
} else {
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
// parentCancelCtx follows a chain of parent references until it finds a
// *cancelCtx. This function understands how each of the concrete types in this
// package represents its parent.
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
return nil, false
}
}
}
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context
done chan struct{} // closed by the first cancel call.
mu sync.Mutex
children map[canceler]bool // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
func (c *cancelCtx) Done() <-chan struct{} {
return c.done
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.err
}
func (c *cancelCtx) String() string {
return fmt.Sprintf("%v.WithCancel", c.Context)
}
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
close(c.done)
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: deadline,
}
propagateCancel(parent, c)
d := deadline.Sub(time.Now())
if d <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(true, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) String() string {
return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
func WithValue(parent Context, key interface{}, val interface{}) Context {
return &valueCtx{parent, key, val}
}
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) String() string {
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}

109
vendor/golang.org/x/net/context/pre_go19.go generated vendored Normal file
View File

@@ -0,0 +1,109 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !go1.9
package context
import "time"
// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
//
// WithCancel arranges for Done to be closed when cancel is called;
// WithDeadline arranges for Done to be closed when the deadline
// expires; WithTimeout arranges for Done to be closed when the timeout
// elapses.
//
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// See http://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancelation.
Done() <-chan struct{}
// Err returns a non-nil error value after Done is closed. Err returns
// Canceled if the context was canceled or DeadlineExceeded if the
// context's deadline passed. No other values for Err are defined.
// After Done is closed, successive calls to Err return the same value.
Err() error
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
//
// A key identifies a specific value in a Context. Functions that wish
// to store values in Context typically allocate a key in a global
// variable then use that key as the argument to context.WithValue and
// Context.Value. A key can be any type that supports equality;
// packages should define keys as an unexported type to avoid
// collisions.
//
// Packages that define a Context key should provide type-safe accessors
// for the values stores using that key:
//
// // Package user defines a User type that's stored in Contexts.
// package user
//
// import "golang.org/x/net/context"
//
// // User is the type of value stored in the Contexts.
// type User struct {...}
//
// // key is an unexported type for keys defined in this package.
// // This prevents collisions with keys defined in other packages.
// type key int
//
// // userKey is the key for user.User values in Contexts. It is
// // unexported; clients use user.NewContext and user.FromContext
// // instead of using this key directly.
// var userKey key = 0
//
// // NewContext returns a new Context that carries value u.
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext returns the User value stored in ctx, if any.
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
Value(key interface{}) interface{}
}
// A CancelFunc tells an operation to abandon its work.
// A CancelFunc does not wait for the work to stop.
// After the first call, subsequent calls to a CancelFunc do nothing.
type CancelFunc func()

176
vendor/gopkg.in/tomb.v1/tomb.go generated vendored
View File

@@ -1,176 +0,0 @@
// Copyright (c) 2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// The tomb package offers a conventional API for clean goroutine termination.
//
// A Tomb tracks the lifecycle of a goroutine as alive, dying or dead,
// and the reason for its death.
//
// The zero value of a Tomb assumes that a goroutine is about to be
// created or already alive. Once Kill or Killf is called with an
// argument that informs the reason for death, the goroutine is in
// a dying state and is expected to terminate soon. Right before the
// goroutine function or method returns, Done must be called to inform
// that the goroutine is indeed dead and about to stop running.
//
// A Tomb exposes Dying and Dead channels. These channels are closed
// when the Tomb state changes in the respective way. They enable
// explicit blocking until the state changes, and also to selectively
// unblock select statements accordingly.
//
// When the tomb state changes to dying and there's still logic going
// on within the goroutine, nested functions and methods may choose to
// return ErrDying as their error value, as this error won't alter the
// tomb state if provided to the Kill method. This is a convenient way to
// follow standard Go practices in the context of a dying tomb.
//
// For background and a detailed example, see the following blog post:
//
// http://blog.labix.org/2011/10/09/death-of-goroutines-under-control
//
// For a more complex code snippet demonstrating the use of multiple
// goroutines with a single Tomb, see:
//
// http://play.golang.org/p/Xh7qWsDPZP
//
package tomb
import (
"errors"
"fmt"
"sync"
)
// A Tomb tracks the lifecycle of a goroutine as alive, dying or dead,
// and the reason for its death.
//
// See the package documentation for details.
type Tomb struct {
m sync.Mutex
dying chan struct{}
dead chan struct{}
reason error
}
var (
ErrStillAlive = errors.New("tomb: still alive")
ErrDying = errors.New("tomb: dying")
)
func (t *Tomb) init() {
t.m.Lock()
if t.dead == nil {
t.dead = make(chan struct{})
t.dying = make(chan struct{})
t.reason = ErrStillAlive
}
t.m.Unlock()
}
// Dead returns the channel that can be used to wait
// until t.Done has been called.
func (t *Tomb) Dead() <-chan struct{} {
t.init()
return t.dead
}
// Dying returns the channel that can be used to wait
// until t.Kill or t.Done has been called.
func (t *Tomb) Dying() <-chan struct{} {
t.init()
return t.dying
}
// Wait blocks until the goroutine is in a dead state and returns the
// reason for its death.
func (t *Tomb) Wait() error {
t.init()
<-t.dead
t.m.Lock()
reason := t.reason
t.m.Unlock()
return reason
}
// Done flags the goroutine as dead, and should be called a single time
// right before the goroutine function or method returns.
// If the goroutine was not already in a dying state before Done is
// called, it will be flagged as dying and dead at once with no
// error.
func (t *Tomb) Done() {
t.Kill(nil)
close(t.dead)
}
// Kill flags the goroutine as dying for the given reason.
// Kill may be called multiple times, but only the first
// non-nil error is recorded as the reason for termination.
//
// If reason is ErrDying, the previous reason isn't replaced
// even if it is nil. It's a runtime error to call Kill with
// ErrDying if t is not in a dying state.
func (t *Tomb) Kill(reason error) {
t.init()
t.m.Lock()
defer t.m.Unlock()
if reason == ErrDying {
if t.reason == ErrStillAlive {
panic("tomb: Kill with ErrDying while still alive")
}
return
}
if t.reason == nil || t.reason == ErrStillAlive {
t.reason = reason
}
// If the receive on t.dying succeeds, then
// it can only be because we have already closed it.
// If it blocks, then we know that it needs to be closed.
select {
case <-t.dying:
default:
close(t.dying)
}
}
// Killf works like Kill, but builds the reason providing the received
// arguments to fmt.Errorf. The generated error is also returned.
func (t *Tomb) Killf(f string, a ...interface{}) error {
err := fmt.Errorf(f, a...)
t.Kill(err)
return err
}
// Err returns the reason for the goroutine death provided via Kill
// or Killf, or ErrStillAlive when the goroutine is still alive.
func (t *Tomb) Err() (reason error) {
t.init()
t.m.Lock()
reason = t.reason
t.m.Unlock()
return
}

View File

@@ -1,4 +1,4 @@
Installation and usage Installation and usage
---------------------- ----------------------
See [gopkg.in/tomb.v1](https://gopkg.in/tomb.v1) for documentation and usage details. See [gopkg.in/tomb.v2](https://gopkg.in/tomb.v2) for documentation and usage details.

74
vendor/gopkg.in/tomb.v2/context.go generated vendored Normal file
View File

@@ -0,0 +1,74 @@
// +build go1.7
package tomb
import (
"context"
)
// WithContext returns a new tomb that is killed when the provided parent
// context is canceled, and a copy of parent with a replaced Done channel
// that is closed when either the tomb is dying or the parent is canceled.
// The returned context may also be obtained via the tomb's Context method.
func WithContext(parent context.Context) (*Tomb, context.Context) {
var t Tomb
t.init()
if parent.Done() != nil {
go func() {
select {
case <-t.Dying():
case <-parent.Done():
t.Kill(parent.Err())
}
}()
}
t.parent = parent
child, cancel := context.WithCancel(parent)
t.addChild(parent, child, cancel)
return &t, child
}
// Context returns a context that is a copy of the provided parent context with
// a replaced Done channel that is closed when either the tomb is dying or the
// parent is cancelled.
//
// If parent is nil, it defaults to the parent provided via WithContext, or an
// empty background parent if the tomb wasn't created via WithContext.
func (t *Tomb) Context(parent context.Context) context.Context {
t.init()
t.m.Lock()
defer t.m.Unlock()
if parent == nil {
if t.parent == nil {
t.parent = context.Background()
}
parent = t.parent.(context.Context)
}
if child, ok := t.child[parent]; ok {
return child.context.(context.Context)
}
child, cancel := context.WithCancel(parent)
t.addChild(parent, child, cancel)
return child
}
func (t *Tomb) addChild(parent context.Context, child context.Context, cancel func()) {
if t.reason != ErrStillAlive {
cancel()
return
}
if t.child == nil {
t.child = make(map[interface{}]childContext)
}
t.child[parent] = childContext{child, cancel, child.Done()}
for parent, child := range t.child {
select {
case <-child.done:
delete(t.child, parent)
default:
}
}
}

74
vendor/gopkg.in/tomb.v2/context16.go generated vendored Normal file
View File

@@ -0,0 +1,74 @@
// +build !go1.7
package tomb
import (
"golang.org/x/net/context"
)
// WithContext returns a new tomb that is killed when the provided parent
// context is canceled, and a copy of parent with a replaced Done channel
// that is closed when either the tomb is dying or the parent is canceled.
// The returned context may also be obtained via the tomb's Context method.
func WithContext(parent context.Context) (*Tomb, context.Context) {
var t Tomb
t.init()
if parent.Done() != nil {
go func() {
select {
case <-t.Dying():
case <-parent.Done():
t.Kill(parent.Err())
}
}()
}
t.parent = parent
child, cancel := context.WithCancel(parent)
t.addChild(parent, child, cancel)
return &t, child
}
// Context returns a context that is a copy of the provided parent context with
// a replaced Done channel that is closed when either the tomb is dying or the
// parent is cancelled.
//
// If parent is nil, it defaults to the parent provided via WithContext, or an
// empty background parent if the tomb wasn't created via WithContext.
func (t *Tomb) Context(parent context.Context) context.Context {
t.init()
t.m.Lock()
defer t.m.Unlock()
if parent == nil {
if t.parent == nil {
t.parent = context.Background()
}
parent = t.parent.(context.Context)
}
if child, ok := t.child[parent]; ok {
return child.context.(context.Context)
}
child, cancel := context.WithCancel(parent)
t.addChild(parent, child, cancel)
return child
}
func (t *Tomb) addChild(parent context.Context, child context.Context, cancel func()) {
if t.reason != ErrStillAlive {
cancel()
return
}
if t.child == nil {
t.child = make(map[interface{}]childContext)
}
t.child[parent] = childContext{child, cancel, child.Done()}
for parent, child := range t.child {
select {
case <-child.done:
delete(t.child, parent)
default:
}
}
}

237
vendor/gopkg.in/tomb.v2/tomb.go generated vendored Normal file
View File

@@ -0,0 +1,237 @@
// Copyright (c) 2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// The tomb package handles clean goroutine tracking and termination.
//
// The zero value of a Tomb is ready to handle the creation of a tracked
// goroutine via its Go method, and then any tracked goroutine may call
// the Go method again to create additional tracked goroutines at
// any point.
//
// If any of the tracked goroutines returns a non-nil error, or the
// Kill or Killf method is called by any goroutine in the system (tracked
// or not), the tomb Err is set, Alive is set to false, and the Dying
// channel is closed to flag that all tracked goroutines are supposed
// to willingly terminate as soon as possible.
//
// Once all tracked goroutines terminate, the Dead channel is closed,
// and Wait unblocks and returns the first non-nil error presented
// to the tomb via a result or an explicit Kill or Killf method call,
// or nil if there were no errors.
//
// It is okay to create further goroutines via the Go method while
// the tomb is in a dying state. The final dead state is only reached
// once all tracked goroutines terminate, at which point calling
// the Go method again will cause a runtime panic.
//
// Tracked functions and methods that are still running while the tomb
// is in dying state may choose to return ErrDying as their error value.
// This preserves the well established non-nil error convention, but is
// understood by the tomb as a clean termination. The Err and Wait
// methods will still return nil if all observed errors were either
// nil or ErrDying.
//
// For background and a detailed example, see the following blog post:
//
// http://blog.labix.org/2011/10/09/death-of-goroutines-under-control
//
package tomb
import (
"errors"
"fmt"
"sync"
)
// A Tomb tracks the lifecycle of one or more goroutines as alive,
// dying or dead, and the reason for their death.
//
// See the package documentation for details.
type Tomb struct {
m sync.Mutex
alive int
dying chan struct{}
dead chan struct{}
reason error
// context.Context is available in Go 1.7+.
parent interface{}
child map[interface{}]childContext
}
type childContext struct {
context interface{}
cancel func()
done <-chan struct{}
}
var (
ErrStillAlive = errors.New("tomb: still alive")
ErrDying = errors.New("tomb: dying")
)
func (t *Tomb) init() {
t.m.Lock()
if t.dead == nil {
t.dead = make(chan struct{})
t.dying = make(chan struct{})
t.reason = ErrStillAlive
}
t.m.Unlock()
}
// Dead returns the channel that can be used to wait until
// all goroutines have finished running.
func (t *Tomb) Dead() <-chan struct{} {
t.init()
return t.dead
}
// Dying returns the channel that can be used to wait until
// t.Kill is called.
func (t *Tomb) Dying() <-chan struct{} {
t.init()
return t.dying
}
// Wait blocks until all goroutines have finished running, and
// then returns the reason for their death.
func (t *Tomb) Wait() error {
t.init()
<-t.dead
t.m.Lock()
reason := t.reason
t.m.Unlock()
return reason
}
// Go runs f in a new goroutine and tracks its termination.
//
// If f returns a non-nil error, t.Kill is called with that
// error as the death reason parameter.
//
// It is f's responsibility to monitor the tomb and return
// appropriately once it is in a dying state.
//
// It is safe for the f function to call the Go method again
// to create additional tracked goroutines. Once all tracked
// goroutines return, the Dead channel is closed and the
// Wait method unblocks and returns the death reason.
//
// Calling the Go method after all tracked goroutines return
// causes a runtime panic. For that reason, calling the Go
// method a second time out of a tracked goroutine is unsafe.
func (t *Tomb) Go(f func() error) {
t.init()
t.m.Lock()
defer t.m.Unlock()
select {
case <-t.dead:
panic("tomb.Go called after all goroutines terminated")
default:
}
t.alive++
go t.run(f)
}
func (t *Tomb) run(f func() error) {
err := f()
t.m.Lock()
defer t.m.Unlock()
t.alive--
if t.alive == 0 || err != nil {
t.kill(err)
if t.alive == 0 {
close(t.dead)
}
}
}
// Kill puts the tomb in a dying state for the given reason,
// closes the Dying channel, and sets Alive to false.
//
// Althoguh Kill may be called multiple times, only the first
// non-nil error is recorded as the death reason.
//
// If reason is ErrDying, the previous reason isn't replaced
// even if nil. It's a runtime error to call Kill with ErrDying
// if t is not in a dying state.
func (t *Tomb) Kill(reason error) {
t.init()
t.m.Lock()
defer t.m.Unlock()
t.kill(reason)
}
func (t *Tomb) kill(reason error) {
if reason == ErrStillAlive {
panic("tomb: Kill with ErrStillAlive")
}
if reason == ErrDying {
if t.reason == ErrStillAlive {
panic("tomb: Kill with ErrDying while still alive")
}
return
}
if t.reason == ErrStillAlive {
t.reason = reason
close(t.dying)
for _, child := range t.child {
child.cancel()
}
t.child = nil
return
}
if t.reason == nil {
t.reason = reason
return
}
}
// Killf calls the Kill method with an error built providing the received
// parameters to fmt.Errorf. The generated error is also returned.
func (t *Tomb) Killf(f string, a ...interface{}) error {
err := fmt.Errorf(f, a...)
t.Kill(err)
return err
}
// Err returns the death reason, or ErrStillAlive if the tomb
// is not in a dying or dead state.
func (t *Tomb) Err() (reason error) {
t.init()
t.m.Lock()
reason = t.reason
t.m.Unlock()
return
}
// Alive returns true if the tomb is not in a dying or dead state.
func (t *Tomb) Alive() bool {
return t.Err() == ErrStillAlive
}

7
vendor/modules.txt vendored
View File

@@ -174,6 +174,9 @@ golang.org/x/crypto/pbkdf2
golang.org/x/exp/constraints golang.org/x/exp/constraints
golang.org/x/exp/rand golang.org/x/exp/rand
golang.org/x/exp/slices golang.org/x/exp/slices
# golang.org/x/net v0.26.0
## explicit; go 1.18
golang.org/x/net/context
# golang.org/x/sys v0.22.0 # golang.org/x/sys v0.22.0
## explicit; go 1.18 ## explicit; go 1.18
golang.org/x/sys/execabs golang.org/x/sys/execabs
@@ -223,6 +226,6 @@ google.golang.org/protobuf/reflect/protoregistry
google.golang.org/protobuf/runtime/protoiface google.golang.org/protobuf/runtime/protoiface
google.golang.org/protobuf/runtime/protoimpl google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/types/known/timestamppb google.golang.org/protobuf/types/known/timestamppb
# gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 # gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
## explicit ## explicit
gopkg.in/tomb.v1 gopkg.in/tomb.v2