2014-11-16 20:13:20 +00:00
|
|
|
// Copyright (C) 2014 The Syncthing Authors.
|
2014-09-29 19:43:32 +00:00
|
|
|
//
|
2015-03-07 20:36:35 +00:00
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
2017-02-09 06:52:18 +00:00
|
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
2014-07-25 12:50:14 +00:00
|
|
|
|
2014-07-13 19:07:24 +00:00
|
|
|
// Package events provides event subscription and polling functionality.
|
|
|
|
package events
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2016-08-08 18:09:40 +00:00
|
|
|
"runtime"
|
2014-07-13 19:07:24 +00:00
|
|
|
"time"
|
2015-04-22 22:54:31 +00:00
|
|
|
|
2015-08-06 09:29:25 +00:00
|
|
|
"github.com/syncthing/syncthing/lib/sync"
|
2014-07-13 19:07:24 +00:00
|
|
|
)
|
|
|
|
|
2015-01-18 01:12:06 +00:00
|
|
|
type EventType int
|
2014-07-13 19:07:24 +00:00
|
|
|
|
|
|
|
const (
|
2017-01-31 12:04:29 +00:00
|
|
|
Starting EventType = 1 << iota
|
2014-07-13 19:07:24 +00:00
|
|
|
StartupComplete
|
2014-09-28 11:00:38 +00:00
|
|
|
DeviceDiscovered
|
|
|
|
DeviceConnected
|
|
|
|
DeviceDisconnected
|
|
|
|
DeviceRejected
|
2015-08-23 19:56:10 +00:00
|
|
|
DevicePaused
|
|
|
|
DeviceResumed
|
2016-05-19 07:01:43 +00:00
|
|
|
LocalChangeDetected
|
2016-12-21 16:35:20 +00:00
|
|
|
RemoteChangeDetected
|
2014-07-13 19:07:24 +00:00
|
|
|
LocalIndexUpdated
|
|
|
|
RemoteIndexUpdated
|
|
|
|
ItemStarted
|
2015-02-01 17:31:19 +00:00
|
|
|
ItemFinished
|
2014-07-17 11:38:36 +00:00
|
|
|
StateChanged
|
2014-09-28 11:00:38 +00:00
|
|
|
FolderRejected
|
2014-09-06 15:31:23 +00:00
|
|
|
ConfigSaved
|
2014-11-16 23:18:59 +00:00
|
|
|
DownloadProgress
|
2016-05-22 07:52:08 +00:00
|
|
|
RemoteDownloadProgress
|
2015-03-26 22:26:51 +00:00
|
|
|
FolderSummary
|
|
|
|
FolderCompletion
|
2015-06-26 11:31:30 +00:00
|
|
|
FolderErrors
|
2015-08-26 22:49:06 +00:00
|
|
|
FolderScanProgress
|
2016-12-21 18:41:25 +00:00
|
|
|
FolderPaused
|
|
|
|
FolderResumed
|
2016-05-04 19:38:12 +00:00
|
|
|
ListenAddressesChanged
|
2015-11-08 20:05:36 +00:00
|
|
|
LoginAttempt
|
2014-07-13 19:07:24 +00:00
|
|
|
|
2014-10-06 22:03:24 +00:00
|
|
|
AllEvents = (1 << iota) - 1
|
2014-07-13 19:07:24 +00:00
|
|
|
)
|
|
|
|
|
2016-08-08 18:09:40 +00:00
|
|
|
var runningTests = false
|
|
|
|
|
2017-02-07 07:25:09 +00:00
|
|
|
const eventLogTimeout = 15 * time.Millisecond
|
|
|
|
|
2014-07-13 19:07:24 +00:00
|
|
|
func (t EventType) String() string {
|
|
|
|
switch t {
|
2014-07-17 11:38:36 +00:00
|
|
|
case Starting:
|
|
|
|
return "Starting"
|
2014-07-13 19:07:24 +00:00
|
|
|
case StartupComplete:
|
|
|
|
return "StartupComplete"
|
2014-09-28 11:00:38 +00:00
|
|
|
case DeviceDiscovered:
|
|
|
|
return "DeviceDiscovered"
|
|
|
|
case DeviceConnected:
|
|
|
|
return "DeviceConnected"
|
|
|
|
case DeviceDisconnected:
|
|
|
|
return "DeviceDisconnected"
|
|
|
|
case DeviceRejected:
|
|
|
|
return "DeviceRejected"
|
2016-05-19 07:01:43 +00:00
|
|
|
case LocalChangeDetected:
|
|
|
|
return "LocalChangeDetected"
|
2016-12-21 16:35:20 +00:00
|
|
|
case RemoteChangeDetected:
|
|
|
|
return "RemoteChangeDetected"
|
2014-07-13 19:07:24 +00:00
|
|
|
case LocalIndexUpdated:
|
|
|
|
return "LocalIndexUpdated"
|
|
|
|
case RemoteIndexUpdated:
|
|
|
|
return "RemoteIndexUpdated"
|
|
|
|
case ItemStarted:
|
|
|
|
return "ItemStarted"
|
2015-02-01 17:31:19 +00:00
|
|
|
case ItemFinished:
|
|
|
|
return "ItemFinished"
|
2014-07-17 11:38:36 +00:00
|
|
|
case StateChanged:
|
|
|
|
return "StateChanged"
|
2014-09-28 11:00:38 +00:00
|
|
|
case FolderRejected:
|
|
|
|
return "FolderRejected"
|
2014-09-06 15:31:23 +00:00
|
|
|
case ConfigSaved:
|
|
|
|
return "ConfigSaved"
|
2014-11-16 23:18:59 +00:00
|
|
|
case DownloadProgress:
|
|
|
|
return "DownloadProgress"
|
2016-05-22 07:52:08 +00:00
|
|
|
case RemoteDownloadProgress:
|
|
|
|
return "RemoteDownloadProgress"
|
2015-03-26 22:26:51 +00:00
|
|
|
case FolderSummary:
|
|
|
|
return "FolderSummary"
|
|
|
|
case FolderCompletion:
|
|
|
|
return "FolderCompletion"
|
2015-06-26 11:31:30 +00:00
|
|
|
case FolderErrors:
|
|
|
|
return "FolderErrors"
|
2015-08-23 19:56:10 +00:00
|
|
|
case DevicePaused:
|
|
|
|
return "DevicePaused"
|
|
|
|
case DeviceResumed:
|
|
|
|
return "DeviceResumed"
|
2015-08-26 22:49:06 +00:00
|
|
|
case FolderScanProgress:
|
|
|
|
return "FolderScanProgress"
|
2016-12-21 18:41:25 +00:00
|
|
|
case FolderPaused:
|
|
|
|
return "FolderPaused"
|
|
|
|
case FolderResumed:
|
|
|
|
return "FolderResumed"
|
2016-05-04 19:38:12 +00:00
|
|
|
case ListenAddressesChanged:
|
|
|
|
return "ListenAddressesChanged"
|
2015-11-08 20:05:36 +00:00
|
|
|
case LoginAttempt:
|
|
|
|
return "LoginAttempt"
|
2014-07-13 19:07:24 +00:00
|
|
|
default:
|
|
|
|
return "Unknown"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t EventType) MarshalText() ([]byte, error) {
|
|
|
|
return []byte(t.String()), nil
|
|
|
|
}
|
|
|
|
|
2017-04-13 17:14:34 +00:00
|
|
|
func UnmarshalEventType(s string) EventType {
|
|
|
|
switch s {
|
|
|
|
case "Starting":
|
|
|
|
return Starting
|
|
|
|
case "StartupComplete":
|
|
|
|
return StartupComplete
|
|
|
|
case "DeviceDiscovered":
|
|
|
|
return DeviceDiscovered
|
|
|
|
case "DeviceConnected":
|
|
|
|
return DeviceConnected
|
|
|
|
case "DeviceDisconnected":
|
|
|
|
return DeviceDisconnected
|
|
|
|
case "DeviceRejected":
|
|
|
|
return DeviceRejected
|
|
|
|
case "LocalChangeDetected":
|
|
|
|
return LocalChangeDetected
|
|
|
|
case "RemoteChangeDetected":
|
|
|
|
return RemoteChangeDetected
|
|
|
|
case "LocalIndexUpdated":
|
|
|
|
return LocalIndexUpdated
|
|
|
|
case "RemoteIndexUpdated":
|
|
|
|
return RemoteIndexUpdated
|
|
|
|
case "ItemStarted":
|
|
|
|
return ItemStarted
|
|
|
|
case "ItemFinished":
|
|
|
|
return ItemFinished
|
|
|
|
case "StateChanged":
|
|
|
|
return StateChanged
|
|
|
|
case "FolderRejected":
|
|
|
|
return FolderRejected
|
|
|
|
case "ConfigSaved":
|
|
|
|
return ConfigSaved
|
|
|
|
case "DownloadProgress":
|
|
|
|
return DownloadProgress
|
|
|
|
case "RemoteDownloadProgress":
|
|
|
|
return RemoteDownloadProgress
|
|
|
|
case "FolderSummary":
|
|
|
|
return FolderSummary
|
|
|
|
case "FolderCompletion":
|
|
|
|
return FolderCompletion
|
|
|
|
case "FolderErrors":
|
|
|
|
return FolderErrors
|
|
|
|
case "DevicePaused":
|
|
|
|
return DevicePaused
|
|
|
|
case "DeviceResumed":
|
|
|
|
return DeviceResumed
|
|
|
|
case "FolderScanProgress":
|
|
|
|
return FolderScanProgress
|
|
|
|
case "FolderPaused":
|
|
|
|
return FolderPaused
|
|
|
|
case "FolderResumed":
|
|
|
|
return FolderResumed
|
|
|
|
case "ListenAddressesChanged":
|
|
|
|
return ListenAddressesChanged
|
|
|
|
case "LoginAttempt":
|
|
|
|
return LoginAttempt
|
|
|
|
default:
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-13 19:07:24 +00:00
|
|
|
const BufferSize = 64
|
|
|
|
|
|
|
|
type Logger struct {
|
2016-06-27 21:18:58 +00:00
|
|
|
subs []*Subscription
|
|
|
|
nextSubscriptionIDs []int
|
|
|
|
nextGlobalID int
|
2017-02-07 07:25:09 +00:00
|
|
|
timeout *time.Timer
|
2016-06-27 21:18:58 +00:00
|
|
|
mutex sync.Mutex
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type Event struct {
|
2016-06-27 21:18:58 +00:00
|
|
|
// Per-subscription sequential event ID. Named "id" for backwards compatibility with the REST API
|
|
|
|
SubscriptionID int `json:"id"`
|
|
|
|
// Global ID of the event across all subscriptions
|
|
|
|
GlobalID int `json:"globalID"`
|
|
|
|
Time time.Time `json:"time"`
|
|
|
|
Type EventType `json:"type"`
|
|
|
|
Data interface{} `json:"data"`
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type Subscription struct {
|
2015-05-23 18:38:41 +00:00
|
|
|
mask EventType
|
|
|
|
events chan Event
|
|
|
|
timeout *time.Timer
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var Default = NewLogger()
|
|
|
|
|
|
|
|
var (
|
|
|
|
ErrTimeout = errors.New("timeout")
|
|
|
|
ErrClosed = errors.New("closed")
|
|
|
|
)
|
|
|
|
|
|
|
|
func NewLogger() *Logger {
|
2017-02-07 07:25:09 +00:00
|
|
|
l := &Logger{
|
|
|
|
mutex: sync.NewMutex(),
|
|
|
|
timeout: time.NewTimer(time.Second),
|
|
|
|
}
|
|
|
|
// Make sure the timer is in the stopped state and hasn't fired anything
|
|
|
|
// into the channel.
|
|
|
|
if !l.timeout.Stop() {
|
|
|
|
<-l.timeout.C
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
2017-02-07 07:25:09 +00:00
|
|
|
return l
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Logger) Log(t EventType, data interface{}) {
|
|
|
|
l.mutex.Lock()
|
2016-06-27 21:18:58 +00:00
|
|
|
l.nextGlobalID++
|
2017-03-07 05:44:47 +00:00
|
|
|
dl.Debugln("log", l.nextGlobalID, t, data)
|
2016-06-27 21:18:58 +00:00
|
|
|
|
2014-07-13 19:07:24 +00:00
|
|
|
e := Event{
|
2016-06-27 21:18:58 +00:00
|
|
|
GlobalID: l.nextGlobalID,
|
|
|
|
Time: time.Now(),
|
|
|
|
Type: t,
|
|
|
|
Data: data,
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
2016-06-27 21:18:58 +00:00
|
|
|
|
|
|
|
for i, s := range l.subs {
|
2014-07-13 19:07:24 +00:00
|
|
|
if s.mask&t != 0 {
|
2016-06-27 21:18:58 +00:00
|
|
|
e.SubscriptionID = l.nextSubscriptionIDs[i]
|
|
|
|
l.nextSubscriptionIDs[i]++
|
|
|
|
|
2017-02-07 07:25:09 +00:00
|
|
|
l.timeout.Reset(eventLogTimeout)
|
|
|
|
timedOut := false
|
|
|
|
|
2014-07-13 19:07:24 +00:00
|
|
|
select {
|
|
|
|
case s.events <- e:
|
2017-02-07 07:25:09 +00:00
|
|
|
case <-l.timeout.C:
|
2014-10-06 22:03:24 +00:00
|
|
|
// if s.events is not ready, drop the event
|
2017-02-07 07:25:09 +00:00
|
|
|
timedOut = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// If stop returns false it already sent something to the
|
|
|
|
// channel. If we didn't already read it above we must do so now
|
|
|
|
// or we get a spurious timeout on the next loop.
|
|
|
|
if !l.timeout.Stop() && !timedOut {
|
|
|
|
<-l.timeout.C
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
l.mutex.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Logger) Subscribe(mask EventType) *Subscription {
|
|
|
|
l.mutex.Lock()
|
2015-10-03 15:25:21 +00:00
|
|
|
dl.Debugln("subscribe", mask)
|
2015-11-17 11:03:18 +00:00
|
|
|
|
2014-07-13 19:07:24 +00:00
|
|
|
s := &Subscription{
|
2015-05-23 18:38:41 +00:00
|
|
|
mask: mask,
|
|
|
|
events: make(chan Event, BufferSize),
|
|
|
|
timeout: time.NewTimer(0),
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
2015-11-17 11:03:18 +00:00
|
|
|
|
|
|
|
// We need to create the timeout timer in the stopped, non-fired state so
|
|
|
|
// that Subscription.Poll() can safely reset it and select on the timeout
|
|
|
|
// channel. This ensures the timer is stopped and the channel drained.
|
2016-08-08 18:09:40 +00:00
|
|
|
if runningTests {
|
|
|
|
// Make the behavior stable when running tests to avoid randomly
|
|
|
|
// varying test coverage. This ensures, in practice if not in
|
|
|
|
// theory, that the timer fires and we take the true branch of the
|
|
|
|
// next if.
|
|
|
|
runtime.Gosched()
|
|
|
|
}
|
2015-11-17 11:03:18 +00:00
|
|
|
if !s.timeout.Stop() {
|
|
|
|
<-s.timeout.C
|
|
|
|
}
|
|
|
|
|
2015-09-29 15:17:09 +00:00
|
|
|
l.subs = append(l.subs, s)
|
2016-06-27 21:18:58 +00:00
|
|
|
l.nextSubscriptionIDs = append(l.nextSubscriptionIDs, 1)
|
2014-07-13 19:07:24 +00:00
|
|
|
l.mutex.Unlock()
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Logger) Unsubscribe(s *Subscription) {
|
|
|
|
l.mutex.Lock()
|
2015-10-03 15:25:21 +00:00
|
|
|
dl.Debugln("unsubscribe")
|
2015-09-29 15:17:09 +00:00
|
|
|
for i, ss := range l.subs {
|
|
|
|
if s == ss {
|
|
|
|
last := len(l.subs) - 1
|
2016-06-27 21:18:58 +00:00
|
|
|
|
2015-09-29 15:17:09 +00:00
|
|
|
l.subs[i] = l.subs[last]
|
|
|
|
l.subs[last] = nil
|
|
|
|
l.subs = l.subs[:last]
|
2016-06-27 21:18:58 +00:00
|
|
|
|
|
|
|
l.nextSubscriptionIDs[i] = l.nextSubscriptionIDs[last]
|
|
|
|
l.nextSubscriptionIDs[last] = 0
|
|
|
|
l.nextSubscriptionIDs = l.nextSubscriptionIDs[:last]
|
|
|
|
|
2015-09-29 15:17:09 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2014-07-13 19:07:24 +00:00
|
|
|
close(s.events)
|
|
|
|
l.mutex.Unlock()
|
|
|
|
}
|
|
|
|
|
2015-05-23 18:38:41 +00:00
|
|
|
// Poll returns an event from the subscription or an error if the poll times
|
|
|
|
// out of the event channel is closed. Poll should not be called concurrently
|
|
|
|
// from multiple goroutines for a single subscription.
|
2014-07-13 19:07:24 +00:00
|
|
|
func (s *Subscription) Poll(timeout time.Duration) (Event, error) {
|
2015-10-03 15:25:21 +00:00
|
|
|
dl.Debugln("poll", timeout)
|
2014-07-25 12:50:14 +00:00
|
|
|
|
2015-11-17 11:03:18 +00:00
|
|
|
s.timeout.Reset(timeout)
|
2015-08-24 07:38:39 +00:00
|
|
|
|
2014-07-13 19:07:24 +00:00
|
|
|
select {
|
|
|
|
case e, ok := <-s.events:
|
|
|
|
if !ok {
|
|
|
|
return e, ErrClosed
|
|
|
|
}
|
2016-08-08 18:09:40 +00:00
|
|
|
if runningTests {
|
|
|
|
// Make the behavior stable when running tests to avoid randomly
|
|
|
|
// varying test coverage. This ensures, in practice if not in
|
|
|
|
// theory, that the timer fires and we take the true branch of
|
|
|
|
// the next if.
|
|
|
|
s.timeout.Reset(0)
|
|
|
|
runtime.Gosched()
|
|
|
|
}
|
2015-11-17 11:03:18 +00:00
|
|
|
if !s.timeout.Stop() {
|
|
|
|
// The timeout must be stopped and possibly drained to be ready
|
|
|
|
// for reuse in the next call.
|
|
|
|
<-s.timeout.C
|
|
|
|
}
|
2014-07-13 19:07:24 +00:00
|
|
|
return e, nil
|
2015-05-23 18:38:41 +00:00
|
|
|
case <-s.timeout.C:
|
2014-07-13 19:07:24 +00:00
|
|
|
return Event{}, ErrTimeout
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-26 23:12:12 +00:00
|
|
|
func (s *Subscription) C() <-chan Event {
|
|
|
|
return s.events
|
|
|
|
}
|
|
|
|
|
2016-03-21 19:36:08 +00:00
|
|
|
type bufferedSubscription struct {
|
2014-07-13 19:07:24 +00:00
|
|
|
sub *Subscription
|
|
|
|
buf []Event
|
|
|
|
next int
|
2016-06-27 21:18:58 +00:00
|
|
|
cur int // Current SubscriptionID
|
2014-07-13 19:07:24 +00:00
|
|
|
mut sync.Mutex
|
2017-01-31 12:04:29 +00:00
|
|
|
cond *sync.TimeoutCond
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
|
|
|
|
2016-03-21 19:36:08 +00:00
|
|
|
type BufferedSubscription interface {
|
2017-01-31 12:04:29 +00:00
|
|
|
Since(id int, into []Event, timeout time.Duration) []Event
|
2016-03-21 19:36:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewBufferedSubscription(s *Subscription, size int) BufferedSubscription {
|
|
|
|
bs := &bufferedSubscription{
|
2014-07-13 19:07:24 +00:00
|
|
|
sub: s,
|
|
|
|
buf: make([]Event, size),
|
2015-04-22 22:54:31 +00:00
|
|
|
mut: sync.NewMutex(),
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
2017-01-31 12:04:29 +00:00
|
|
|
bs.cond = sync.NewTimeoutCond(bs.mut)
|
2014-07-13 19:07:24 +00:00
|
|
|
go bs.pollingLoop()
|
|
|
|
return bs
|
|
|
|
}
|
|
|
|
|
2016-03-21 19:36:08 +00:00
|
|
|
func (s *bufferedSubscription) pollingLoop() {
|
2017-02-04 15:53:39 +00:00
|
|
|
for ev := range s.sub.C() {
|
2014-07-13 19:07:24 +00:00
|
|
|
s.mut.Lock()
|
|
|
|
s.buf[s.next] = ev
|
|
|
|
s.next = (s.next + 1) % len(s.buf)
|
2016-06-27 21:18:58 +00:00
|
|
|
s.cur = ev.SubscriptionID
|
2014-07-13 19:07:24 +00:00
|
|
|
s.cond.Broadcast()
|
|
|
|
s.mut.Unlock()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-31 12:04:29 +00:00
|
|
|
func (s *bufferedSubscription) Since(id int, into []Event, timeout time.Duration) []Event {
|
2014-07-13 19:07:24 +00:00
|
|
|
s.mut.Lock()
|
|
|
|
defer s.mut.Unlock()
|
|
|
|
|
2017-01-31 12:04:29 +00:00
|
|
|
// Check once first before generating the TimeoutCondWaiter
|
|
|
|
if id >= s.cur {
|
|
|
|
waiter := s.cond.SetupWait(timeout)
|
|
|
|
defer waiter.Stop()
|
|
|
|
|
|
|
|
for id >= s.cur {
|
|
|
|
if eventsAvailable := waiter.Wait(); !eventsAvailable {
|
|
|
|
// Timed out
|
|
|
|
return into
|
|
|
|
}
|
|
|
|
}
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for i := s.next; i < len(s.buf); i++ {
|
2016-06-27 21:18:58 +00:00
|
|
|
if s.buf[i].SubscriptionID > id {
|
2014-07-13 19:07:24 +00:00
|
|
|
into = append(into, s.buf[i])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for i := 0; i < s.next; i++ {
|
2016-06-27 21:18:58 +00:00
|
|
|
if s.buf[i].SubscriptionID > id {
|
2014-07-13 19:07:24 +00:00
|
|
|
into = append(into, s.buf[i])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return into
|
|
|
|
}
|
2015-05-27 09:14:39 +00:00
|
|
|
|
|
|
|
// Error returns a string pointer suitable for JSON marshalling errors. It
|
2015-11-12 02:20:34 +00:00
|
|
|
// retains the "null on success" semantics, but ensures the error result is a
|
2015-05-27 09:14:39 +00:00
|
|
|
// string regardless of the underlying concrete error type.
|
|
|
|
func Error(err error) *string {
|
|
|
|
if err == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
str := err.Error()
|
|
|
|
return &str
|
|
|
|
}
|