Do not os.Exit() on "wire" and xmpp-proto errors, re-run main routine and helper goroutines
This commit is contained in:
29
buny.go
29
buny.go
@@ -1,7 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -84,8 +85,17 @@ func buny(v xmpp.Presence) error { //nolint:gocognit
|
||||
"http://jabber.org/protocol/muc#admin",
|
||||
ban,
|
||||
); err != nil {
|
||||
log.Errorf("Unable to ban user: id=%s, err=%s", id, err)
|
||||
os.Exit(1)
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Unable to ban user: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
gTomb.Kill(err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -121,8 +131,17 @@ func buny(v xmpp.Presence) error { //nolint:gocognit
|
||||
"http://jabber.org/protocol/muc#admin",
|
||||
ban,
|
||||
); err != nil {
|
||||
log.Errorf("Unable to ban user: id=%s, err=%s", id, err)
|
||||
os.Exit(1)
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Unable to ban user: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
gTomb.Kill(err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
134
event_parser.go
134
event_parser.go
@@ -2,8 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -111,8 +111,17 @@ func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
xmpp.IQTypeResult,
|
||||
answer,
|
||||
); err != nil {
|
||||
log.Errorf("Unable to send version info to jabber server: id=%s, err=%s", id, err)
|
||||
os.Exit(1)
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Unable to send version info to jabber server: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
@@ -128,8 +137,17 @@ func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
xmpp.IQTypeResult,
|
||||
"",
|
||||
); err != nil {
|
||||
log.Errorf("Unable to send pong to jabber server: id=%s, err=%s", id, err)
|
||||
os.Exit(1)
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Unable to send pong to jabber server: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
@@ -151,8 +169,17 @@ func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
xmpp.IQTypeResult,
|
||||
answer,
|
||||
); err != nil {
|
||||
log.Errorf("Unable to send last activity time to jabber server: id=%s, err=%s", id, err)
|
||||
os.Exit(1)
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Unable to send last activity time to jabber server: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
@@ -176,8 +203,17 @@ func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
xmpp.IQTypeResult,
|
||||
answer,
|
||||
); err != nil {
|
||||
log.Errorf("Unable to send disco#info to jabber server: id=%s, err=%s", id, err)
|
||||
os.Exit(1)
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Unable to send disco#info to jabber server: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
@@ -199,8 +235,17 @@ func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
xmpp.IQTypeError,
|
||||
answer,
|
||||
); err != nil {
|
||||
log.Errorf("Unable to send disco#items to jabber server: id=%s, err=%s", id, err)
|
||||
os.Exit(1)
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Unable to send disco#items to jabber server: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
@@ -237,8 +282,17 @@ func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
xmpp.IQTypeResult,
|
||||
answer,
|
||||
); err != nil {
|
||||
log.Errorf("Unable to send urn:xmpp:time to jabber server: id=%s, err=%s", id, err)
|
||||
os.Exit(1)
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Unable to send urn:xmpp:time to jabber server: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
@@ -275,13 +329,17 @@ func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
xmpp.IQTypeError,
|
||||
answer,
|
||||
); err != nil {
|
||||
log.Errorf(
|
||||
"Unable to send pubsub feature unimplemented to jabber server: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Unable to send pubsub feature unimplemented to jabber server: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
os.Exit(1)
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
@@ -312,9 +370,17 @@ func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
xmpp.IQTypeResult,
|
||||
"",
|
||||
); err != nil {
|
||||
log.Errorf("Unable to send pong to jabber server: id=%s, err=%s", id, err)
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Unable to send pong to jabber server: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
os.Exit(1)
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,12 +460,17 @@ func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
xmpp.IQTypeError,
|
||||
answer,
|
||||
); err != nil {
|
||||
log.Errorf("Unable to send set feature unimplemented to jabber server: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Unable to send set feature unimplemented to jabber server: id=%s, err=%s",
|
||||
id,
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
os.Exit(1)
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug(spew.Sdump(e))
|
||||
@@ -449,9 +520,16 @@ func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
time.Sleep(time.Duration(config.Jabber.MucRejoinDelay) * time.Second)
|
||||
|
||||
if _, err := talk.JoinMUCNoHistory(iqErrorCancelNotAcceptable.By, nick); err != nil {
|
||||
log.Errorf("Looks like connection to server also lost")
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Looks like connection to server also lost err=%s",
|
||||
err,
|
||||
),
|
||||
)
|
||||
|
||||
os.Exit(1)
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Error(
|
||||
@@ -519,7 +597,9 @@ func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
}
|
||||
|
||||
if err := buny(v); err != nil {
|
||||
os.Exit(1)
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
10
globals.go
10
globals.go
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/eleksir/go-xmpp"
|
||||
"gopkg.in/tomb.v1"
|
||||
)
|
||||
|
||||
// Config - это у нас глобальная штука :) .
|
||||
@@ -45,16 +46,16 @@ var lastServerActivity int64
|
||||
|
||||
// Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из
|
||||
// muc-ов.
|
||||
var lastMucActivity = NewCollection()
|
||||
var lastMucActivity *Collection
|
||||
|
||||
// Получен ли ответ на запрос disco#info к серверу.
|
||||
var serverCapsQueried bool
|
||||
|
||||
// sync.Map-ка с капабилити сервера.
|
||||
var serverCapsList = NewCollection()
|
||||
var serverCapsList *Collection
|
||||
|
||||
// sync.Map-ка с комнатами и их capability.
|
||||
var mucCapsList = NewCollection()
|
||||
var mucCapsList *Collection
|
||||
|
||||
// Время, когда был отправлен c2s ping.
|
||||
var serverPingTimestampTx int64
|
||||
@@ -62,4 +63,7 @@ var serverPingTimestampTx int64
|
||||
// Время, когда был принят s2c pong.
|
||||
var serverPingTimestampRx int64
|
||||
|
||||
// Объектик для хранения стейта утилизатора горутинок
|
||||
var gTomb tomb.Tomb
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
|
||||
1
go.mod
1
go.mod
@@ -8,6 +8,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-20230728194245-b0cb94b80691
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
2
go.sum
2
go.sum
@@ -20,5 +20,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
272
lib.go
272
lib.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
@@ -452,20 +453,15 @@ func establishConnection() {
|
||||
talk, err = options.NewClient()
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
||||
os.Exit(1)
|
||||
gTomb.Kill(err)
|
||||
return
|
||||
}
|
||||
|
||||
// По идее keepalive должен же проходить только, если мы уже на сервере, так?
|
||||
if _, err := talk.SendKeepAlive(); err != nil {
|
||||
log.Errorf("Try to send initial KeepAlive, got error: %s", err)
|
||||
time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second)
|
||||
|
||||
// Понятно, что мы уже не в процессе установления соединения, а произошла ошибка
|
||||
connecting = false
|
||||
|
||||
os.Exit(1)
|
||||
gTomb.Kill(err)
|
||||
}
|
||||
|
||||
log.Info("Connected")
|
||||
@@ -481,8 +477,9 @@ func establishConnection() {
|
||||
},
|
||||
); err != nil {
|
||||
log.Infof("Unable to send presence to jabber server: %s", err)
|
||||
gTomb.Kill(err)
|
||||
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
lastActivity = time.Now().Unix()
|
||||
@@ -495,8 +492,9 @@ func establishConnection() {
|
||||
|
||||
if err != nil {
|
||||
log.Infof("Unable to send disco#info to jabber server: %s", err)
|
||||
gTomb.Kill(err)
|
||||
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,7 +505,7 @@ func joinMuc(room string) {
|
||||
if _, err := talk.DiscoverInfo(talk.JID(), room); err != nil {
|
||||
log.Infof("Unable to send disco#info to MUC %s: %s", room, err)
|
||||
|
||||
os.Exit(1)
|
||||
gTomb.Kill(err)
|
||||
}
|
||||
|
||||
// Ждём, пока muc нам вернёт список фичей.
|
||||
@@ -539,7 +537,7 @@ func joinMuc(room string) {
|
||||
if _, err := talk.JoinMUCNoHistory(room, config.Jabber.Nick); err != nil {
|
||||
log.Errorf("Unable to join to MUC: %s", room)
|
||||
|
||||
os.Exit(1)
|
||||
gTomb.Kill(err)
|
||||
}
|
||||
|
||||
log.Infof("Joining to MUC: %s", room)
|
||||
@@ -577,95 +575,114 @@ func joinMuc(room string) {
|
||||
},
|
||||
); err != nil {
|
||||
log.Infof("Unable to send presence to MUC %s: %s", room, err)
|
||||
os.Exit(1)
|
||||
gTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// probeServerLiveness() проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой
|
||||
// они выкидывают клиента через некоторое время неактивности.
|
||||
func probeServerLiveness() { //nolint:gocognit
|
||||
defer gTomb.Done()
|
||||
|
||||
for {
|
||||
if shutdown {
|
||||
select {
|
||||
case <-gTomb.Dying():
|
||||
return
|
||||
}
|
||||
|
||||
sleepTime := time.Duration(config.Jabber.ServerPingDelay) * 1000 * time.Millisecond
|
||||
sleepTime += time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
|
||||
time.Sleep(sleepTime)
|
||||
default:
|
||||
for {
|
||||
if shutdown {
|
||||
return
|
||||
}
|
||||
|
||||
if !isConnected {
|
||||
continue
|
||||
}
|
||||
sleepTime := time.Duration(config.Jabber.ServerPingDelay) * 1000 * time.Millisecond
|
||||
sleepTime += time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
|
||||
time.Sleep(sleepTime)
|
||||
|
||||
// Пингуем, только если не было никакой активности в течение > config.Jabber.ServerPingDelay,
|
||||
// в худшем случе это будет ~ (config.Jabber.PingSplayDelay * 2) + config.Jabber.PingSplayDelay
|
||||
// if (time.Now().Unix() - lastServerActivity) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
|
||||
// continue
|
||||
// }
|
||||
if !isConnected {
|
||||
continue
|
||||
}
|
||||
|
||||
if serverCapsQueried { // Сервер ответил на disco#info
|
||||
var (
|
||||
value interface{}
|
||||
exist bool
|
||||
)
|
||||
// Пингуем, только если не было никакой активности в течение > config.Jabber.ServerPingDelay,
|
||||
// в худшем случе это будет ~ (config.Jabber.PingSplayDelay * 2) + config.Jabber.PingSplayDelay
|
||||
// if (time.Now().Unix() - lastServerActivity) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
|
||||
// continue
|
||||
// }
|
||||
|
||||
value, exist = serverCapsList.Get("urn:xmpp:ping")
|
||||
if serverCapsQueried { // Сервер ответил на disco#info
|
||||
var (
|
||||
value interface{}
|
||||
exist bool
|
||||
)
|
||||
|
||||
switch {
|
||||
// Сервер анонсировал, что умеет в c2s пинги
|
||||
case exist && value.(bool):
|
||||
// Таймаут c2s пинга. Возьмём сумму задержки между пингами, добавим таймаут коннекта и добавим
|
||||
// максимальную корректировку разброса.
|
||||
txTimeout := config.Jabber.ServerPingDelay + config.Jabber.ConnectionTimeout
|
||||
txTimeout += config.Jabber.PingSplayDelay
|
||||
rxTimeout := txTimeout
|
||||
value, exist = serverCapsList.Get("urn:xmpp:ping")
|
||||
|
||||
rxTimeAgo := time.Now().Unix() - serverPingTimestampRx
|
||||
|
||||
if serverPingTimestampTx > 0 { // Первая пуля от нас ушла...
|
||||
switch {
|
||||
// Давненько мы не получали понгов от сервера, вероятно, соединение с сервером утеряно?
|
||||
case rxTimeAgo > (rxTimeout * 2):
|
||||
log.Debugf(
|
||||
"Stall connection detected. No c2s pong for %d seconds",
|
||||
rxTimeAgo,
|
||||
)
|
||||
// Сервер анонсировал, что умеет в c2s пинги
|
||||
case exist && value.(bool):
|
||||
// Таймаут c2s пинга. Возьмём сумму задержки между пингами, добавим таймаут коннекта и добавим
|
||||
// максимальную корректировку разброса.
|
||||
txTimeout := config.Jabber.ServerPingDelay + config.Jabber.ConnectionTimeout
|
||||
txTimeout += config.Jabber.PingSplayDelay
|
||||
rxTimeout := txTimeout
|
||||
|
||||
os.Exit(1)
|
||||
rxTimeAgo := time.Now().Unix() - serverPingTimestampRx
|
||||
|
||||
// По-умолчанию, мы отправляем c2s пинг
|
||||
default:
|
||||
log.Debugf("Sending c2s ping from %s to %s", talk.JID(), config.Jabber.Server)
|
||||
if serverPingTimestampTx > 0 { // Первая пуля от нас ушла...
|
||||
switch {
|
||||
// Давненько мы не получали понгов от сервера, вероятно, соединение с сервером утеряно?
|
||||
case rxTimeAgo > (rxTimeout * 2):
|
||||
err := errors.New(
|
||||
fmt.Sprintf(
|
||||
"Stall connection detected. No c2s pong for %d seconds",
|
||||
rxTimeAgo,
|
||||
),
|
||||
)
|
||||
|
||||
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
|
||||
os.Exit(1)
|
||||
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)
|
||||
|
||||
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
|
||||
gTomb.Kill(err)
|
||||
continue
|
||||
}
|
||||
|
||||
serverPingTimestampTx = time.Now().Unix()
|
||||
}
|
||||
|
||||
serverPingTimestampTx = time.Now().Unix()
|
||||
// Сервер не анонсировал, что умеет в c2s пинги
|
||||
default:
|
||||
log.Debug("Sending keepalive whitespace ping")
|
||||
|
||||
if _, err := talk.SendKeepAlive(); err != nil {
|
||||
gTomb.Kill(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else { // Первая пуля пока не вылетела, отправляем
|
||||
log.Debugf("Sending first c2s ping from %s to %s", talk.JID(), config.Jabber.Server)
|
||||
} else { // Сервер не ответил на disco#info
|
||||
log.Debug("Sending keepalive whitespace ping")
|
||||
|
||||
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
|
||||
os.Exit(1)
|
||||
if _, err := talk.SendKeepAlive(); err != nil {
|
||||
gTomb.Kill(err)
|
||||
continue
|
||||
}
|
||||
|
||||
serverPingTimestampTx = time.Now().Unix()
|
||||
}
|
||||
|
||||
// Сервер не анонсировал, что умеет в c2s пинги
|
||||
default:
|
||||
log.Debug("Sending keepalive whitespace ping")
|
||||
|
||||
if _, err := talk.SendKeepAlive(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
} else { // Сервер не ответил на disco#info
|
||||
log.Debug("Sending keepalive whitespace ping")
|
||||
|
||||
if _, err := talk.SendKeepAlive(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -673,58 +690,69 @@ func probeServerLiveness() { //nolint:gocognit
|
||||
|
||||
// probeMUCLiveness() Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е.
|
||||
func probeMUCLiveness() {
|
||||
defer gTomb.Done()
|
||||
|
||||
for {
|
||||
for _, room := range roomsConnected {
|
||||
var (
|
||||
exist bool
|
||||
lastActivityTS interface{}
|
||||
)
|
||||
select {
|
||||
case <-gTomb.Dying():
|
||||
return
|
||||
|
||||
// Если записи про комнату нету, то пинговать её бессмысленно.
|
||||
if lastActivityTS, exist = lastMucActivity.Get(room); !exist {
|
||||
continue
|
||||
}
|
||||
default:
|
||||
for {
|
||||
for _, room := range roomsConnected {
|
||||
var (
|
||||
exist bool
|
||||
lastActivityTS interface{}
|
||||
)
|
||||
|
||||
// Если время последней активности в чятике не превысило
|
||||
// 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+"/"+config.Jabber.Nick); err != nil {
|
||||
reEstablishConnection()
|
||||
}
|
||||
}(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) {
|
||||
// Небольшая рандомная задержка перед пингом комнаты.
|
||||
sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
|
||||
time.Sleep(sleepTime)
|
||||
|
||||
log.Debugf("Sending MUC ping from %s to %s", talk.JID(), room)
|
||||
|
||||
if err := talk.PingS2S(talk.JID(), room); err != nil {
|
||||
os.Exit(1)
|
||||
// Если записи про комнату нету, то пинговать её бессмысленно.
|
||||
if lastActivityTS, exist = lastMucActivity.Get(room); !exist {
|
||||
continue
|
||||
}
|
||||
}(room)
|
||||
|
||||
// Если время последней активности в чятике не превысило
|
||||
// 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+"/"+config.Jabber.Nick); 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) {
|
||||
// Небольшая рандомная задержка перед пингом комнаты.
|
||||
sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
|
||||
time.Sleep(sleepTime)
|
||||
|
||||
log.Debugf("Sending MUC ping from %s to %s", talk.JID(), room)
|
||||
|
||||
if err := talk.PingS2S(talk.JID(), room); err != nil {
|
||||
gTomb.Kill(err)
|
||||
}
|
||||
}(room)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(config.Jabber.MucPingDelay) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(config.Jabber.MucPingDelay) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
166
main.go
166
main.go
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/eleksir/go-xmpp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/tomb.v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -43,12 +44,6 @@ func init() {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
}
|
||||
|
||||
// Пока про capabilities мы ничего не знаем
|
||||
serverCapsQueried = false
|
||||
|
||||
// Мы пока что не пинговали сервер.
|
||||
serverPingTimestampRx = 0
|
||||
serverPingTimestampTx = 0
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -79,77 +74,122 @@ func main() {
|
||||
// github.com/mattn/go-xmpp пишет в stdio, нам этого не надо, ловим выхлоп его в logrus с уровнем trace
|
||||
xmpp.DebugWriter = log.WithFields(log.Fields{"logger": "stdlib"}).WriterLevel(log.TraceLevel)
|
||||
|
||||
options = &xmpp.Options{ //nolint:exhaustruct
|
||||
Host: fmt.Sprintf("%s:%d", config.Jabber.Server, config.Jabber.Port),
|
||||
User: config.Jabber.User,
|
||||
Password: config.Jabber.Password,
|
||||
Resource: config.Jabber.Resource,
|
||||
NoTLS: !config.Jabber.Ssl,
|
||||
StartTLS: config.Jabber.StartTLS,
|
||||
TLSConfig: &tls.Config{ //nolint:exhaustruct
|
||||
ServerName: config.Jabber.Server,
|
||||
InsecureSkipVerify: !config.Jabber.SslVerify, //nolint:gosec
|
||||
},
|
||||
InsecureAllowUnencryptedAuth: config.Jabber.InsecureAllowUnencryptedAuth,
|
||||
Debug: verboseClient,
|
||||
Session: false,
|
||||
Status: "xa",
|
||||
StatusMessage: fmt.Sprintf("%s bot is starting up", config.Jabber.Nick),
|
||||
DialTimeout: time.Duration(config.Jabber.ConnectionTimeout) * time.Second,
|
||||
}
|
||||
|
||||
// Хэндлер сигналов не надо трогать, он нужен для завершения программы целиком
|
||||
go sigHandler()
|
||||
signal.Notify(sigChan, os.Interrupt)
|
||||
// Установим коннект
|
||||
establishConnection()
|
||||
|
||||
serverPingTimestampRx = time.Now().Unix() // Считаем, что если коннект запустился, то первый пинг успешен
|
||||
|
||||
// Тыкаем сервер палочкой, проверяем, что коннект жив и переустанавливаем его, если он не жив
|
||||
go probeServerLiveness()
|
||||
|
||||
// Тыкаем muc-и палочкой, проверяем, что они живы и пере-заходим в них, если пинги пропали
|
||||
go probeMUCLiveness()
|
||||
|
||||
// Гребём сообщения...
|
||||
for {
|
||||
// Стриггерилось завершение работы приложения, или соединение не установлено (порвалось, например)
|
||||
// грести не надо
|
||||
if shutdown {
|
||||
break
|
||||
options = &xmpp.Options{ //nolint:exhaustruct
|
||||
Host: fmt.Sprintf("%s:%d", config.Jabber.Server, config.Jabber.Port),
|
||||
User: config.Jabber.User,
|
||||
Password: config.Jabber.Password,
|
||||
Resource: config.Jabber.Resource,
|
||||
NoTLS: !config.Jabber.Ssl,
|
||||
StartTLS: config.Jabber.StartTLS,
|
||||
TLSConfig: &tls.Config{ //nolint:exhaustruct
|
||||
ServerName: config.Jabber.Server,
|
||||
InsecureSkipVerify: !config.Jabber.SslVerify, //nolint:gosec
|
||||
},
|
||||
InsecureAllowUnencryptedAuth: config.Jabber.InsecureAllowUnencryptedAuth,
|
||||
Debug: verboseClient,
|
||||
Session: false,
|
||||
Status: "xa",
|
||||
StatusMessage: fmt.Sprintf("%s bot is starting up", config.Jabber.Nick),
|
||||
DialTimeout: time.Duration(config.Jabber.ConnectionTimeout) * time.Second,
|
||||
}
|
||||
|
||||
if !isConnected {
|
||||
continue
|
||||
}
|
||||
// Через tomb попробуем попробуем сделать выход горутинок управляемым
|
||||
gTomb = tomb.Tomb{}
|
||||
|
||||
chat, err := talk.Recv()
|
||||
// Устанавливаем соединение и гребём события, посылаемые сервером
|
||||
myLoop()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to get events from server: %s", err)
|
||||
log.Error(gTomb.Wait())
|
||||
time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
// Стрим не читается, он закрылся с той стороны во время чтения
|
||||
case errors.Is(err, io.EOF):
|
||||
os.Exit(1)
|
||||
func myLoop() {
|
||||
defer gTomb.Done()
|
||||
|
||||
// Пытаемся читать закрытый сокет
|
||||
case errors.Is(err, net.ErrClosed):
|
||||
os.Exit(1)
|
||||
for {
|
||||
select {
|
||||
case <-gTomb.Dying():
|
||||
return
|
||||
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
|
||||
|
||||
// Не смогли записать в сокет
|
||||
case errors.Is(err, net.ErrWriteToConnected):
|
||||
os.Exit(1)
|
||||
// Установим коннект
|
||||
establishConnection()
|
||||
|
||||
// Не сетевая проблема
|
||||
default:
|
||||
// Это уже что-то странное.
|
||||
// Вероятно, ошибка парсинга xml. Собственно, баг сервера, тут мы ничего поделать не можем
|
||||
os.Exit(1)
|
||||
serverPingTimestampRx = time.Now().Unix() // Считаем, что если коннект запустился, то первый пинг успешен
|
||||
|
||||
// Тыкаем сервер палочкой, проверяем, что коннект жив и переустанавливаем его, если он не жив
|
||||
go probeServerLiveness()
|
||||
|
||||
// Тыкаем muc-и палочкой, проверяем, что они живы и пере-заходим в них, если пинги пропали
|
||||
go probeMUCLiveness()
|
||||
|
||||
// Гребём сообщения...
|
||||
for {
|
||||
// Стриггерилось завершение работы приложения, или соединение не установлено (порвалось, например)
|
||||
// грести не надо
|
||||
if shutdown {
|
||||
break
|
||||
}
|
||||
|
||||
if !isConnected {
|
||||
continue
|
||||
}
|
||||
|
||||
chat, err := talk.Recv()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to get events from server: %s", err)
|
||||
|
||||
switch {
|
||||
// Стрим не читается, он закрылся с той стороны во время чтения
|
||||
case errors.Is(err, io.EOF):
|
||||
err := errors.New("Tcp stream closed while reading")
|
||||
gTomb.Kill(err)
|
||||
continue
|
||||
|
||||
// Пытаемся читать закрытый сокет
|
||||
case errors.Is(err, net.ErrClosed):
|
||||
err := errors.New("Unable to read closed socket")
|
||||
gTomb.Kill(err)
|
||||
continue
|
||||
|
||||
// Не смогли записать в сокет
|
||||
case errors.Is(err, net.ErrWriteToConnected):
|
||||
err := errors.New("Unable to write to socket")
|
||||
gTomb.Kill(err)
|
||||
continue
|
||||
|
||||
// Не сетевая проблема
|
||||
default:
|
||||
// Это уже что-то странное.
|
||||
// Вероятно, ошибка парсинга xml. Собственно, баг сервера, тут мы ничего поделать не можем
|
||||
err := errors.New("error during parsing received message")
|
||||
gTomb.Kill(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
parseEvent(chat)
|
||||
}
|
||||
}
|
||||
|
||||
parseEvent(chat)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
vendor/gopkg.in/tomb.v1/LICENSE
generated
vendored
Normal file
29
vendor/gopkg.in/tomb.v1/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
tomb - support for clean goroutine termination in Go.
|
||||
|
||||
Copyright (c) 2010-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.
|
||||
4
vendor/gopkg.in/tomb.v1/README.md
generated
vendored
Normal file
4
vendor/gopkg.in/tomb.v1/README.md
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
Installation and usage
|
||||
----------------------
|
||||
|
||||
See [gopkg.in/tomb.v1](https://gopkg.in/tomb.v1) for documentation and usage details.
|
||||
176
vendor/gopkg.in/tomb.v1/tomb.go
generated
vendored
Normal file
176
vendor/gopkg.in/tomb.v1/tomb.go
generated
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
// 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
|
||||
}
|
||||
3
vendor/modules.txt
vendored
3
vendor/modules.txt
vendored
@@ -22,3 +22,6 @@ golang.org/x/exp/slices
|
||||
golang.org/x/sys/internal/unsafeheader
|
||||
golang.org/x/sys/unix
|
||||
golang.org/x/sys/windows
|
||||
# gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
|
||||
## explicit
|
||||
gopkg.in/tomb.v1
|
||||
|
||||
Reference in New Issue
Block a user