Use logrus to handle logs
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit adds the logrus module, and improves logs handling for the entire program. Error logs are better displayed, and the log level can be set from the configuration file.
This commit is contained in:
parent
93e8bc0f52
commit
51bc7cb3a0
7 changed files with 87 additions and 11 deletions
|
@ -41,7 +41,10 @@ For now, the program is configured through a JSON configuration file. Here is a
|
||||||
"token": "<your_bot_token>",
|
"token": "<your_bot_token>",
|
||||||
"channel_id": 9999999
|
"channel_id": 9999999
|
||||||
},
|
},
|
||||||
"polling_frequency": 5
|
"polling_frequency": 5,
|
||||||
|
"log": {
|
||||||
|
"level": "info"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,11 @@ import (
|
||||||
|
|
||||||
"git.faercol.me/faercol/public-ip-tracker/tracker/config"
|
"git.faercol.me/faercol/public-ip-tracker/tracker/config"
|
||||||
"git.faercol.me/faercol/public-ip-tracker/tracker/ip"
|
"git.faercol.me/faercol/public-ip-tracker/tracker/ip"
|
||||||
|
"git.faercol.me/faercol/public-ip-tracker/tracker/logger"
|
||||||
"github.com/ahugues/go-telegram-api/bot"
|
"github.com/ahugues/go-telegram-api/bot"
|
||||||
"github.com/ahugues/go-telegram-api/notifier"
|
"github.com/ahugues/go-telegram-api/notifier"
|
||||||
"github.com/ahugues/go-telegram-api/structs"
|
"github.com/ahugues/go-telegram-api/structs"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Notifier struct {
|
type Notifier struct {
|
||||||
|
@ -26,21 +28,26 @@ type Notifier struct {
|
||||||
errChan chan error
|
errChan chan error
|
||||||
changesChan chan net.IP
|
changesChan chan net.IP
|
||||||
exitChan chan struct{}
|
exitChan chan struct{}
|
||||||
|
logger logrus.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) SendInitMessage() error {
|
func (n *Notifier) SendInitMessage() error {
|
||||||
|
n.logger.Debug("Getting current public IP")
|
||||||
publicIP, err := n.ipGetter.GetCurrentPublicIP(n.ctx)
|
publicIP, err := n.ipGetter.GetCurrentPublicIP(n.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get current public IP: %w", err)
|
return fmt.Errorf("failed to get current public IP: %w", err)
|
||||||
}
|
}
|
||||||
|
n.logger.Debugf("Current public IP is %s", publicIP.String())
|
||||||
|
|
||||||
n.currentIP = publicIP
|
n.currentIP = publicIP
|
||||||
currentTime := n.timeGetter()
|
currentTime := n.timeGetter()
|
||||||
|
|
||||||
|
n.logger.Debug("Sending init message to Telegram")
|
||||||
initMsg := fmt.Sprintf("Public IP tracker initialized at %v, public IP is %s", currentTime, publicIP)
|
initMsg := fmt.Sprintf("Public IP tracker initialized at %v, public IP is %s", currentTime, publicIP)
|
||||||
if err := n.tgBot.SendMessage(n.ctx, n.tgChatID, initMsg); err != nil {
|
if err := n.tgBot.SendMessage(n.ctx, n.tgChatID, initMsg); err != nil {
|
||||||
return fmt.Errorf("failed to send initialization message: %w", err)
|
return fmt.Errorf("failed to send initialization message: %w", err)
|
||||||
}
|
}
|
||||||
|
n.logger.Debug("Message sent")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +68,7 @@ func (n *Notifier) sendCurrentIP() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) watchTG() {
|
func (n *Notifier) watchTG() {
|
||||||
|
n.logger.Debug("Subscribing to messages notificator")
|
||||||
id, updateChan := n.tgWatcher.Subscribe([]structs.UpdateType{structs.UpdateMessage})
|
id, updateChan := n.tgWatcher.Subscribe([]structs.UpdateType{structs.UpdateMessage})
|
||||||
go n.tgWatcher.Run(n.ctx)
|
go n.tgWatcher.Run(n.ctx)
|
||||||
|
|
||||||
|
@ -81,21 +89,29 @@ func (n *Notifier) watchTG() {
|
||||||
|
|
||||||
func (n *Notifier) Run() {
|
func (n *Notifier) Run() {
|
||||||
go n.watchTG()
|
go n.watchTG()
|
||||||
|
n.logger.Infof("Start watching for public IP changes, polling frequency is %v", n.frequency)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-time.After(n.frequency):
|
case <-time.After(n.frequency):
|
||||||
|
n.logger.Debug("Checking if current IP has changed")
|
||||||
newIP, err := n.ipGetter.GetCurrentPublicIP(n.ctx)
|
newIP, err := n.ipGetter.GetCurrentPublicIP(n.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.errChan <- fmt.Errorf("failed to update public IP: %w", err)
|
n.errChan <- fmt.Errorf("failed to update public IP: %w", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
n.logger.Debugf("Got new public IP %s", newIP.String())
|
||||||
if !newIP.Equal(n.currentIP) {
|
if !newIP.Equal(n.currentIP) {
|
||||||
|
n.logger.Debug("New public IP is different from previous IP")
|
||||||
|
n.logger.Warnf("Public IP has changed from %s to %s", n.currentIP.String(), newIP.String())
|
||||||
n.currentIP = newIP
|
n.currentIP = newIP
|
||||||
if err := n.sendUpdatedIPMsg(); err != nil {
|
if err := n.sendUpdatedIPMsg(); err != nil {
|
||||||
n.errChan <- err
|
n.errChan <- err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
n.logger.Debug("Public IP has not changed")
|
||||||
}
|
}
|
||||||
case <-n.ctx.Done():
|
case <-n.ctx.Done():
|
||||||
|
n.logger.Info("Stopping notification daemon")
|
||||||
n.exitChan <- struct{}{}
|
n.exitChan <- struct{}{}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -125,5 +141,6 @@ func New(ctx context.Context, config *config.Config) *Notifier {
|
||||||
exitChan: make(chan struct{}, 1),
|
exitChan: make(chan struct{}, 1),
|
||||||
frequency: config.PollingFrequency,
|
frequency: config.PollingFrequency,
|
||||||
tgWatcher: notifier.New(config.Telegram.Token),
|
tgWatcher: notifier.New(config.Telegram.Token),
|
||||||
|
logger: logger.L,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TelegramConfig struct {
|
type TelegramConfig struct {
|
||||||
|
@ -12,14 +14,33 @@ type TelegramConfig struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogConfig struct {
|
||||||
|
Level logrus.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonLogConfig struct {
|
||||||
|
Level string `json:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Telegram *TelegramConfig
|
Telegram *TelegramConfig
|
||||||
PollingFrequency time.Duration
|
PollingFrequency time.Duration
|
||||||
|
Log *LogConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsonConfig struct {
|
type jsonConfig struct {
|
||||||
Telegram *TelegramConfig `json:"telegram"`
|
Telegram *TelegramConfig `json:"telegram"`
|
||||||
PollingFrequency int64 `json:"polling_frequency"`
|
PollingFrequency int64 `json:"polling_frequency"`
|
||||||
|
Log *jsonLogConfig `json:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLevel(lvlStr string) logrus.Level {
|
||||||
|
for _, lvl := range logrus.AllLevels {
|
||||||
|
if lvl.String() == lvlStr {
|
||||||
|
return lvl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return logrus.InfoLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(filepath string) (*Config, error) {
|
func New(filepath string) (*Config, error) {
|
||||||
|
@ -34,5 +55,8 @@ func New(filepath string) (*Config, error) {
|
||||||
return &Config{
|
return &Config{
|
||||||
Telegram: jsonConf.Telegram,
|
Telegram: jsonConf.Telegram,
|
||||||
PollingFrequency: time.Duration(jsonConf.PollingFrequency) * time.Second,
|
PollingFrequency: time.Duration(jsonConf.PollingFrequency) * time.Second,
|
||||||
|
Log: &LogConfig{
|
||||||
|
Level: parseLevel(jsonConf.Log.Level),
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,4 +5,5 @@ go 1.16
|
||||||
require (
|
require (
|
||||||
github.com/ahugues/go-telegram-api v0.1.0
|
github.com/ahugues/go-telegram-api v0.1.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
|
github.com/sirupsen/logrus v1.9.0
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
github.com/ahugues/go-telegram-api v0.1.0 h1:CGJG0WR282O0hAO9JH2RutPj+Vn+Q+zbjdSHjuvN5yY=
|
github.com/ahugues/go-telegram-api v0.1.0 h1:CGJG0WR282O0hAO9JH2RutPj+Vn+Q+zbjdSHjuvN5yY=
|
||||||
github.com/ahugues/go-telegram-api v0.1.0/go.mod h1:8I/JWxd9GYM7dHOgGmkRI3Ei1u+nGvzeR2knIMmFw7E=
|
github.com/ahugues/go-telegram-api v0.1.0/go.mod h1:8I/JWxd9GYM7dHOgGmkRI3Ei1u+nGvzeR2knIMmFw7E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
@ -16,6 +26,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
@ -26,3 +38,6 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
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=
|
||||||
|
|
10
tracker/logger/logger.go
Normal file
10
tracker/logger/logger.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
var L logrus.Logger
|
||||||
|
|
||||||
|
func Init(level logrus.Level) {
|
||||||
|
L = *logrus.New()
|
||||||
|
L.SetLevel(level)
|
||||||
|
}
|
|
@ -3,12 +3,12 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
"git.faercol.me/faercol/public-ip-tracker/tracker/bot"
|
"git.faercol.me/faercol/public-ip-tracker/tracker/bot"
|
||||||
"git.faercol.me/faercol/public-ip-tracker/tracker/config"
|
"git.faercol.me/faercol/public-ip-tracker/tracker/config"
|
||||||
|
"git.faercol.me/faercol/public-ip-tracker/tracker/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cliArgs struct {
|
type cliArgs struct {
|
||||||
|
@ -26,45 +26,51 @@ func parseArgs() *cliArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println("Parsing arguments")
|
|
||||||
args := parseArgs()
|
args := parseArgs()
|
||||||
|
|
||||||
mainCtx, cancel := context.WithCancel(context.Background())
|
mainCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
fmt.Println("Parsing config")
|
|
||||||
conf, err := config.New(args.configPath)
|
conf, err := config.New(args.configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Initializing bot")
|
logger.Init(conf.Log.Level)
|
||||||
|
logger.L.Infof("Intialized logger with level %v", conf.Log.Level)
|
||||||
|
|
||||||
|
logger.L.Debug("Initializing notification bot")
|
||||||
notifBot := bot.New(mainCtx, conf)
|
notifBot := bot.New(mainCtx, conf)
|
||||||
|
|
||||||
|
logger.L.Debug("Sending initialization message to Telegram")
|
||||||
if err := notifBot.SendInitMessage(); err != nil {
|
if err := notifBot.SendInitMessage(); err != nil {
|
||||||
panic(err)
|
logger.L.Fatalf("Failed to send an initialization message: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Starting monitoring")
|
logger.L.Debug("Starting IP monitoring")
|
||||||
go notifBot.Run()
|
go notifBot.Run()
|
||||||
|
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
signal.Notify(c, os.Interrupt)
|
signal.Notify(c, os.Interrupt)
|
||||||
|
|
||||||
|
logger.L.Info("Public IP monitoring service is operational")
|
||||||
|
|
||||||
outerloop:
|
outerloop:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-c:
|
case <-c:
|
||||||
fmt.Println("received cancel")
|
logger.L.Info("Stopping IP monitoring service")
|
||||||
cancel()
|
cancel()
|
||||||
break outerloop
|
break outerloop
|
||||||
case err := <-notifBot.ErrChan():
|
case err := <-notifBot.ErrChan():
|
||||||
fmt.Printf("Unexpected error %s", err.Error())
|
logger.L.Error(err.Error())
|
||||||
case <-notifBot.Exit():
|
case <-notifBot.Exit():
|
||||||
fmt.Println("Unexpected exit")
|
logger.L.Fatal("Unexpected exit from the monitoring bot")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.L.Debug("Waiting for all services to shut down")
|
||||||
<-notifBot.Exit()
|
<-notifBot.Exit()
|
||||||
|
|
||||||
fmt.Println("OK")
|
logger.L.Info("Public IP monitoring service successfully stopped")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue