From 44004d8fd9f59664fe92ed577087eb262d4d8870 Mon Sep 17 00:00:00 2001 From: eleksir Date: Thu, 2 Jan 2025 17:28:27 +0300 Subject: [PATCH] Use tomb v2, update deps, fix hang on stale connection --- event_parser.go | 2 +- globals.go | 4 +- go.mod | 3 +- go.sum | 2 + lib.go | 332 ++++++++---------- main.go | 277 +++++++-------- redis.go | 6 +- vendor/golang.org/x/net/LICENSE | 27 ++ vendor/golang.org/x/net/PATENTS | 22 ++ vendor/golang.org/x/net/context/context.go | 56 +++ vendor/golang.org/x/net/context/go17.go | 72 ++++ vendor/golang.org/x/net/context/go19.go | 20 ++ vendor/golang.org/x/net/context/pre_go17.go | 300 ++++++++++++++++ vendor/golang.org/x/net/context/pre_go19.go | 109 ++++++ vendor/gopkg.in/tomb.v1/tomb.go | 176 ---------- vendor/gopkg.in/{tomb.v1 => tomb.v2}/LICENSE | 0 .../gopkg.in/{tomb.v1 => tomb.v2}/README.md | 2 +- vendor/gopkg.in/tomb.v2/context.go | 74 ++++ vendor/gopkg.in/tomb.v2/context16.go | 74 ++++ vendor/gopkg.in/tomb.v2/tomb.go | 237 +++++++++++++ vendor/modules.txt | 7 +- 21 files changed, 1282 insertions(+), 520 deletions(-) create mode 100644 vendor/golang.org/x/net/LICENSE create mode 100644 vendor/golang.org/x/net/PATENTS create mode 100644 vendor/golang.org/x/net/context/context.go create mode 100644 vendor/golang.org/x/net/context/go17.go create mode 100644 vendor/golang.org/x/net/context/go19.go create mode 100644 vendor/golang.org/x/net/context/pre_go17.go create mode 100644 vendor/golang.org/x/net/context/pre_go19.go delete mode 100644 vendor/gopkg.in/tomb.v1/tomb.go rename vendor/gopkg.in/{tomb.v1 => tomb.v2}/LICENSE (100%) rename vendor/gopkg.in/{tomb.v1 => tomb.v2}/README.md (51%) create mode 100644 vendor/gopkg.in/tomb.v2/context.go create mode 100644 vendor/gopkg.in/tomb.v2/context16.go create mode 100644 vendor/gopkg.in/tomb.v2/tomb.go diff --git a/event_parser.go b/event_parser.go index 9ce71c1..c31f192 100644 --- a/event_parser.go +++ b/event_parser.go @@ -516,7 +516,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo } else { log.Errorf("Presence notification, Type: %s, From: %s, Show: %s, Status: %s", v.Type, v.From, v.Show, v.Status) - log.Errorf(spew.Sdump(v)) + log.Error(spew.Sdump(v)) } } else { log.Infof( diff --git a/globals.go b/globals.go index 8bc16df..78b1d93 100644 --- a/globals.go +++ b/globals.go @@ -7,7 +7,7 @@ import ( "github.com/cockroachdb/pebble" "github.com/eleksir/go-xmpp" "github.com/go-redis/redis/v8" - "gopkg.in/tomb.v1" + "gopkg.in/tomb.v2" ) // Config - это у нас глобальная штука :) . @@ -33,7 +33,7 @@ var connecting = false var isConnected = false // Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов. -var sigChan = make(chan os.Signal, 1) +var sigChan chan os.Signal // Основной инстанс xmpp-клиента. var talk *xmpp.Client diff --git a/go.mod b/go.mod index 7663ab6..d5e07ac 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/hjson/hjson-go v3.3.0+incompatible github.com/sirupsen/logrus v1.9.3 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 ( @@ -37,6 +37,7 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/rogpeppe/go-internal v1.12.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/text v0.16.0 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/go.sum b/go.sum index 26d8131..4aa7ef0 100644 --- a/go.sum +++ b/go.sum @@ -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/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.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/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lib.go b/lib.go index 844f308..49d84b5 100644 --- a/lib.go +++ b/lib.go @@ -16,7 +16,7 @@ import ( ) // sigHandler Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения. -func sigHandler() { +func sigHandler() error { log.Debug("Installing signal handler") for s := range sigChan { @@ -86,14 +86,16 @@ func sigHandler() { os.Exit(0) } + + return nil } // establishConnection устанавливает соединение с jabber-сервером. -func establishConnection() { +func establishConnection() error { var err error if connecting && !isConnected { - return + return nil } // Проставляем глобальные переменные. @@ -104,26 +106,22 @@ func establishConnection() { talk, err = options.NewClient() if err != nil { - gTomb.Kill(err) - - return + return fmt.Errorf("Unable to connect to %s: %w", options.Host, err) } // По идее keepalive должен же проходить только, если мы уже на сервере, так? if _, err := talk.SendKeepAlive(); err != nil { - log.Errorf("Try to send initial KeepAlive, got error: %s", err) - - gTomb.Kill(err) + return fmt.Errorf("Try to send initial KeepAlive, got error: %w", err) } log.Info("Connected") // Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили 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() connecting = false @@ -134,21 +132,18 @@ func establishConnection() { _, err = talk.DiscoverInfo(talk.JID(), config.Jabber.Server) if err != nil { - log.Infof("Unable to send disco#info to jabber server: %s", err) - gTomb.Kill(err) - - return + return fmt.Errorf("Unable to send disco#info to jabber server: %s", err) } + + return nil } // joinMu джойнится к конференциям/каналам/комнатам в джаббере. -func joinMuc(room string) { +func joinMuc(room string) error { log.Debugf("Sending disco#info from %s to %s", talk.JID(), room) if _, err := talk.DiscoverInfo(talk.JID(), room); err != nil { - log.Infof("Unable to send disco#info to MUC %s: %s", room, err) - - gTomb.Kill(err) + return fmt.Errorf("Unable to send disco#info to MUC %s: %s", room, err) } // Ждём, пока 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 :)") - return + return nil } } // Пытаемся зайти в комнату if _, err := talk.JoinMUCNoHistory(room, getBotNickFromRoomConfig(room)); err != nil { - log.Errorf("Unable to join to MUC: %s", room) - - gTomb.Kill(err) + return fmt.Errorf("Unable to join to MUC: %s, %w", room, err) } log.Infof("Joining to MUC: %s", room) @@ -206,172 +199,152 @@ func joinMuc(room string) { 20*int(config.Jabber.ConnectionTimeout)+1, ) - return + return nil } // Вот теперь точно можно слать статус. log.Infof("Joined to MUC: %s", room) - go RotateStatus(room) + gTomb.Go(func() error { return RotateStatus(room) }) + + return nil } // probeServerLiveness проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой // они выкидывают клиента через некоторое время неактивности. -func probeServerLiveness() { //nolint:gocognit +func probeServerLiveness() error { //nolint:gocognit for { - select { - case <-gTomb.Dying(): - gTomb.Done() - return + for { + if shutdown { + return nil + } - default: - for { - if shutdown { - return - } + sleepTime := time.Duration(config.Jabber.ServerPingDelay) * 1000 * time.Millisecond + sleepTime += time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond + time.Sleep(sleepTime) - sleepTime := time.Duration(config.Jabber.ServerPingDelay) * 1000 * time.Millisecond - sleepTime += time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond - time.Sleep(sleepTime) + if !isConnected { + continue + } - if !isConnected { - continue - } + // Пингуем, только если не было никакой активности в течение > config.Jabber.ServerPingDelay, + // в худшем случе это будет ~ (config.Jabber.PingSplayDelay * 2) + config.Jabber.PingSplayDelay + // if (time.Now().Unix() - lastServerActivity) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) { + // continue + // } - // Пингуем, только если не было никакой активности в течение > config.Jabber.ServerPingDelay, - // в худшем случе это будет ~ (config.Jabber.PingSplayDelay * 2) + config.Jabber.PingSplayDelay - // if (time.Now().Unix() - lastServerActivity) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) { - // continue - // } + if serverCapsQueried { // Сервер ответил на disco#info + var ( + value interface{} + exist bool + ) - if serverCapsQueried { // Сервер ответил на disco#info - var ( - value interface{} - exist bool - ) + value, exist = serverCapsList.Get("urn:xmpp:ping") - 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 { - // Сервер анонсировал, что умеет в c2s пинги - case exist && value.(bool): - // Таймаут c2s пинга. Возьмём сумму задержки между пингами, добавим таймаут коннекта и добавим - // максимальную корректировку разброса. - txTimeout := config.Jabber.ServerPingDelay + config.Jabber.ConnectionTimeout - txTimeout += config.Jabber.PingSplayDelay - rxTimeout := txTimeout + rxTimeAgo := time.Now().Unix() - serverPingTimestampRx - 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 { // Первая пуля от нас ушла... - switch { - // Давненько мы не получали понгов от сервера, вероятно, соединение с сервером утеряно? - case rxTimeAgo > (rxTimeout * 2): - err := fmt.Errorf( //nolint:goerr113 - "stall connection detected. No c2s pong for %d seconds", - rxTimeAgo, - ) + return err - gTomb.Kill(err) - - continue - - // По-умолчанию, мы отправляем 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) + // По-умолчанию, мы отправляем 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 + return err } serverPingTimestampTx = time.Now().Unix() } + } else { // Первая пуля пока не вылетела, отправляем + log.Debugf("Sending first c2s ping from %s to %s", talk.JID(), config.Jabber.Server) - // Сервер не анонсировал, что умеет в c2s пинги - default: - log.Debug("Sending keepalive whitespace ping") - - if _, err := talk.SendKeepAlive(); err != nil { - gTomb.Kill(err) - - continue + if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil { + return err } + + serverPingTimestampTx = time.Now().Unix() } - } else { // Сервер не ответил на disco#info + + // Сервер не анонсировал, что умеет в c2s пинги + default: log.Debug("Sending keepalive whitespace ping") if _, err := talk.SendKeepAlive(); err != nil { - gTomb.Kill(err) - - continue + return err } } + } else { // Сервер не ответил на disco#info + log.Debug("Sending keepalive whitespace ping") + + if _, err := talk.SendKeepAlive(); err != nil { + return err + } } } } } // probeMUCLiveness Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е. -func probeMUCLiveness() { //nolint:gocognit +func probeMUCLiveness() error { //nolint:gocognit for { - select { - case <-gTomb.Dying(): - gTomb.Done() - return + for { + for _, room := range roomsConnected { + var ( + exist bool + lastActivityTS interface{} + ) - default: - for { - for _, room := range roomsConnected { - var ( - exist bool - lastActivityTS interface{} - ) + // Если записи про комнату нету, то пинговать её бессмысленно. + if lastActivityTS, exist = lastMucActivity.Get(room); !exist { + continue + } - // Если записи про комнату нету, то пинговать её бессмысленно. - 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 } + }(room) + */ - // Если время последней активности в чятике не превысило - // config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay, ничего не пингуем. - if (time.Now().Unix() - lastActivityTS.(int64)) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) { - continue - } + var roomMap interface{} - /* Пинг MUC-а по сценарию без серверной оптимизации мы реализовывать не будем. Это как-то не надёжно. - go func(room string) { - // Небольшая рандомная задержка перед пингом комнаты - sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec - time.Sleep(sleepTime) + roomMap, exist = mucCapsList.Get(room) - if err := talk.PingS2S(talk.JID(), room+"/"+getBotNickFromRoomConfig(room)); err != nil { - gTomb.Kill(err) - continue - } - }(room) - */ - - 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) { + // Пинги комнаты проводим, только если она записана, как прошедшая disco#info и поддерживающая + // Server Optimization. + if exist && roomMap.(map[string]bool)["http://jabber.org/protocol/muc#self-ping-optimization"] { + gTomb.Go( + func() error { // Небольшая рандомная задержка перед пингом комнаты. sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond time.Sleep(sleepTime) @@ -379,65 +352,54 @@ func probeMUCLiveness() { //nolint:gocognit log.Debugf("Sending MUC ping from %s to %s", talk.JID(), room) if err := talk.PingS2S(talk.JID(), room); err != nil { - err := fmt.Errorf("unable to ping MUC %s: %w", room, err) - gTomb.Kill(err) + return fmt.Errorf("unable to ping MUC %s: %w", room, err) } - }(room) - } - } - time.Sleep(time.Duration(config.Jabber.MucPingDelay) * time.Second) + return nil + }, + ) + } } + + time.Sleep(time.Duration(config.Jabber.MucPingDelay) * time.Second) } } } // RotateStatus периодически изменяет статус бота в MUC-е согласно настройкам из кофига. -func RotateStatus(room string) { +func RotateStatus(room string) error { for { - select { - case <-gTomb.Dying(): - gTomb.Done() - return + // TODO: Переделать на ticker-ы + totalSleepTime := time.Duration(config.Jabber.RuntimeStatus.RotationTime) * time.Second + totalSleepTime += time.Duration(config.Jabber.RuntimeStatus.RotationSplayTime) * time.Second - default: - // TODO: Переделать на ticker-ы - totalSleepTime := time.Duration(config.Jabber.RuntimeStatus.RotationTime) * time.Second - totalSleepTime += time.Duration(config.Jabber.RuntimeStatus.RotationSplayTime) * time.Second + for { + status := randomPhrase(config.Jabber.RuntimeStatus.Text) + log.Debugf("Set status for MUC: %s to: %s", room, status) - for { - status := randomPhrase(config.Jabber.RuntimeStatus.Text) - log.Debugf("Set status for MUC: %s to: %s", room, status) + var p xmpp.Presence - var p xmpp.Presence - - if room != "" { - p = xmpp.Presence{ //nolint:exhaustruct - To: room, - Status: status, - } - } else { - p = xmpp.Presence{ //nolint:exhaustruct - Status: status, - } + if room != "" { + p = xmpp.Presence{ //nolint:exhaustruct + To: room, + Status: status, } - - if _, err := talk.SendPresence(p); err != nil { - log.Infof("Unable to send presence to MUC %s: %s", room, err) - gTomb.Kill(err) - - return + } else { + p = xmpp.Presence{ //nolint:exhaustruct + Status: status, } - - // Если мы не хотим ротировать, то цикл нам тут не нужен, просто выходим. - 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) } } } diff --git a/main.go b/main.go index 045f97b..011588b 100644 --- a/main.go +++ b/main.go @@ -2,10 +2,7 @@ package main import ( "crypto/tls" - "errors" "fmt" - "io" - "net" "os" "os/signal" "path/filepath" @@ -14,10 +11,10 @@ import ( "github.com/eleksir/go-xmpp" "github.com/go-redis/redis/v8" log "github.com/sirupsen/logrus" - "gopkg.in/tomb.v1" + "gopkg.in/tomb.v2" ) -func init() { +func main() { log.SetFormatter(&log.TextFormatter{ //nolint:exhaustruct DisableQuote: true, DisableLevelTruncation: false, @@ -35,73 +32,78 @@ func init() { os.Exit(1) } - configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath)) - configLocations = append(configLocations, configJSONPath) + for { + sigChan = make(chan os.Signal, 1) + talk = new(xmpp.Client) + options = new(xmpp.Options) - if err := readConfig(); err != nil { - log.Error(err) + configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath)) + configLocations = append(configLocations, configJSONPath) - os.Exit(1) - } + if err := readConfig(); err != nil { + log.Error(err) - // 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) - } -} - -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) + os.Exit(1) } - 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 { - verboseClient = true - } + log.SetOutput(logfile) + } - // github.com/mattn/go-xmpp пишет в stdio, нам этого не надо, ловим выхлоп его в logrus с уровнем trace. - xmpp.DebugWriter = log.WithFields(log.Fields{"logger": "stdlib"}).WriterLevel(log.TraceLevel) + myLogLevel := log.GetLevel() + log.Warnf("Loglevel set to %v", myLogLevel) - // Хэндлер сигналов не надо трогать, он нужен для завершения программы целиком. - go sigHandler() - signal.Notify(sigChan, os.Interrupt) + // Через tomb попробуем сделать выход горутинок управляемым. + gTomb := tomb.Tomb{} - // Иницализируем redis-клиента. - redisClient = redis.NewClient(&redis.Options{ //nolint:exhaustruct - Addr: fmt.Sprintf("%s:%d", config.Redis.Server, config.Redis.Port), - }) + // Иницализируем redis-клиента. + redisClient = redis.NewClient(&redis.Options{ //nolint:exhaustruct + 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) - subscriber = redisClient.Subscribe(ctx, config.Redis.MyChannel) + log.Debugf("Lazy connect() to redis at %s:%d", config.Redis.Server, config.Redis.Port) + 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 Host: fmt.Sprintf("%s:%d", config.Jabber.Server, config.Jabber.Port), User: config.Jabber.User, @@ -121,109 +123,84 @@ func main() { DialTimeout: time.Duration(config.Jabber.ConnectionTimeout) * time.Second, } - // Через tomb попробуем сделать выход горутинок управляемым. - gTomb = tomb.Tomb{} - // Устанавливаем соединение и гребём события, посылаемые сервером. - myLoop() + gTomb.Go(func() error { return myLoop() }) - log.Error(gTomb.Wait()) - - // Если у нас wire error, то вызов .Close() повлечёт за собой ошибку, но мы вынуждены звать .Close(), чтоб - // закрыть tls контекст и почистить всё что связанно с прерванным соединением. - log.Infoln("Closing connection to jabber server") - - _ = talk.Close() + // Ловим первый же kill и не дождаемся остальных, хотя формально надо бы. + _ = <-gTomb.Dying() time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second) } } -func myLoop() { +func myLoop() error { for { - select { - case <-gTomb.Dying(): - gTomb.Done() + // Зададим начальное значение глобальным переменным. + 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() - return + // Установим коннект. + if err := establishConnection(); err != nil { + log.Error(err) - default: - // Зададим начальное значение глобальным переменным. - 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) - } + return err } + + 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 } } diff --git a/redis.go b/redis.go index d81c1e4..901a82c 100644 --- a/redis.go +++ b/redis.go @@ -9,17 +9,19 @@ import ( log "github.com/sirupsen/logrus" ) -func redisLoop(redisMsgChan <-chan *redis.Message) { +func redisLoop(redisMsgChan <-chan *redis.Message) error { // Обработчик событий от редиски for msg := range redisMsgChan { if shutdown { - return + return nil } if err := redisMsgParser(msg.Payload); err != nil { log.Warn(err) } } + + return nil } // redisMsgParser парсит json-чики прилетевшие из REDIS-ки. diff --git a/vendor/golang.org/x/net/LICENSE b/vendor/golang.org/x/net/LICENSE new file mode 100644 index 0000000..6a66aea --- /dev/null +++ b/vendor/golang.org/x/net/LICENSE @@ -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. diff --git a/vendor/golang.org/x/net/PATENTS b/vendor/golang.org/x/net/PATENTS new file mode 100644 index 0000000..7330990 --- /dev/null +++ b/vendor/golang.org/x/net/PATENTS @@ -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. diff --git a/vendor/golang.org/x/net/context/context.go b/vendor/golang.org/x/net/context/context.go new file mode 100644 index 0000000..cf66309 --- /dev/null +++ b/vendor/golang.org/x/net/context/context.go @@ -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 +} diff --git a/vendor/golang.org/x/net/context/go17.go b/vendor/golang.org/x/net/context/go17.go new file mode 100644 index 0000000..0c1b867 --- /dev/null +++ b/vendor/golang.org/x/net/context/go17.go @@ -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) +} diff --git a/vendor/golang.org/x/net/context/go19.go b/vendor/golang.org/x/net/context/go19.go new file mode 100644 index 0000000..e31e35a --- /dev/null +++ b/vendor/golang.org/x/net/context/go19.go @@ -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 diff --git a/vendor/golang.org/x/net/context/pre_go17.go b/vendor/golang.org/x/net/context/pre_go17.go new file mode 100644 index 0000000..065ff3d --- /dev/null +++ b/vendor/golang.org/x/net/context/pre_go17.go @@ -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) +} diff --git a/vendor/golang.org/x/net/context/pre_go19.go b/vendor/golang.org/x/net/context/pre_go19.go new file mode 100644 index 0000000..ec5a638 --- /dev/null +++ b/vendor/golang.org/x/net/context/pre_go19.go @@ -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() diff --git a/vendor/gopkg.in/tomb.v1/tomb.go b/vendor/gopkg.in/tomb.v1/tomb.go deleted file mode 100644 index 9aec56d..0000000 --- a/vendor/gopkg.in/tomb.v1/tomb.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2011 - Gustavo Niemeyer -// -// 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 -} diff --git a/vendor/gopkg.in/tomb.v1/LICENSE b/vendor/gopkg.in/tomb.v2/LICENSE similarity index 100% rename from vendor/gopkg.in/tomb.v1/LICENSE rename to vendor/gopkg.in/tomb.v2/LICENSE diff --git a/vendor/gopkg.in/tomb.v1/README.md b/vendor/gopkg.in/tomb.v2/README.md similarity index 51% rename from vendor/gopkg.in/tomb.v1/README.md rename to vendor/gopkg.in/tomb.v2/README.md index 3ae8788..e7f282b 100644 --- a/vendor/gopkg.in/tomb.v1/README.md +++ b/vendor/gopkg.in/tomb.v2/README.md @@ -1,4 +1,4 @@ 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. diff --git a/vendor/gopkg.in/tomb.v2/context.go b/vendor/gopkg.in/tomb.v2/context.go new file mode 100644 index 0000000..f0fe56f --- /dev/null +++ b/vendor/gopkg.in/tomb.v2/context.go @@ -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: + } + } +} diff --git a/vendor/gopkg.in/tomb.v2/context16.go b/vendor/gopkg.in/tomb.v2/context16.go new file mode 100644 index 0000000..d47d83a --- /dev/null +++ b/vendor/gopkg.in/tomb.v2/context16.go @@ -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: + } + } +} diff --git a/vendor/gopkg.in/tomb.v2/tomb.go b/vendor/gopkg.in/tomb.v2/tomb.go new file mode 100644 index 0000000..069b305 --- /dev/null +++ b/vendor/gopkg.in/tomb.v2/tomb.go @@ -0,0 +1,237 @@ +// Copyright (c) 2011 - Gustavo Niemeyer +// +// 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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ee705b1..8e9a9da 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -174,6 +174,9 @@ golang.org/x/crypto/pbkdf2 golang.org/x/exp/constraints golang.org/x/exp/rand 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 ## explicit; go 1.18 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/protoimpl 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 -gopkg.in/tomb.v1 +gopkg.in/tomb.v2