github.com/glycerine/xcryptossh@v7.0.4+incompatible/idle.go (about)

     1  package ssh
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  	"sync/atomic"
     7  	"time"
     8  )
     9  
    10  // IdleTimer allows a client of the ssh
    11  // library to notice if there has been a
    12  // stall in i/o activity. This enables
    13  // clients to impliment timeout logic
    14  // that works and doesn't timeout under
    15  // long-duration-but-still-successful
    16  // reads/writes.
    17  //
    18  // It is simpler to use the
    19  // SetIdleTimeout(dur time.Duration)
    20  // method on the channel, but
    21  // methods like LastAndMonoNow()
    22  // are also occassionally required.
    23  //
    24  type IdleTimer struct {
    25  	// TimedOut sends empty string if no timeout, else details.
    26  	TimedOut chan string
    27  
    28  	// Halt is the standard means of requesting
    29  	// stop and waiting for that stop to be done.
    30  	Halt *Halter
    31  
    32  	mut       sync.Mutex
    33  	idleDur   time.Duration
    34  	lastStart int64
    35  	lastOK    int64
    36  
    37  	timeoutCallback []func()
    38  
    39  	// GetIdleTimeoutCh returns the current idle timeout duration in use.
    40  	// It will return 0 if timeouts are disabled.
    41  	getIdleTimeoutCh chan time.Duration
    42  
    43  	// SetIdleTimeout() will always set the timeOutRaised state to false.
    44  	// Likewise for sending on setIdleTimeoutCh.
    45  	setIdleTimeoutCh chan *setTimeoutTicket
    46  
    47  	setCallback   chan *callbacks
    48  	addCallback   chan *callbacks
    49  	timeOutRaised string
    50  
    51  	// each of these, for instance,
    52  	// atomicdur is updated atomically, and should
    53  	// be read atomically. For use by AttemptOK) and
    54  	// internal reporting only.
    55  	atomicdur  int64
    56  	overcount  int64
    57  	undercount int64
    58  	beginnano  int64 // not monotonic time source.
    59  
    60  	// if these are not zero, we'll
    61  	// shutdown after receiving an OK.
    62  	// access with atomic.
    63  	isOneshot int32
    64  }
    65  
    66  type callbacks struct {
    67  	onTimeout func()
    68  }
    69  
    70  // NewIdleTimer creates a new IdleTimer which will call
    71  // the `callback` function provided after `dur` inactivity.
    72  // If callback is nil, you must use setTimeoutCallback()
    73  // to establish the callback before activating the timer
    74  // with SetIdleTimeout. The `dur` can be 0 to begin with no
    75  // timeout, in which case the timer will be inactive until
    76  // SetIdleTimeout is called.
    77  func NewIdleTimer(callback func(), dur time.Duration) *IdleTimer {
    78  	t := &IdleTimer{
    79  		getIdleTimeoutCh: make(chan time.Duration),
    80  		setIdleTimeoutCh: make(chan *setTimeoutTicket),
    81  		setCallback:      make(chan *callbacks),
    82  		addCallback:      make(chan *callbacks),
    83  		TimedOut:         make(chan string),
    84  		Halt:             NewHalter(),
    85  	}
    86  	if callback != nil {
    87  		t.timeoutCallback = append(t.timeoutCallback, callback)
    88  	}
    89  	go t.backgroundStart(dur)
    90  	return t
    91  }
    92  
    93  // typically prefer addTimeoutCallback instead; using
    94  // this will blow away any other callbacks that are
    95  // already registered. Unless that is what you want,
    96  // use addTimeoutCallback().
    97  //
    98  func (t *IdleTimer) setTimeoutCallback(timeoutFunc func()) {
    99  	select {
   100  	case t.setCallback <- &callbacks{onTimeout: timeoutFunc}:
   101  	case <-t.Halt.ReqStopChan():
   102  	}
   103  }
   104  
   105  // AddTimeoutCallback adds another callback,
   106  // without removing exiting callbacks
   107  func (t *IdleTimer) AddTimeoutCallback(timeoutFunc func()) {
   108  	if timeoutFunc == nil {
   109  		panic("cannot call addTimeoutCallback with nil function!")
   110  	}
   111  	select {
   112  	case t.addCallback <- &callbacks{onTimeout: timeoutFunc}:
   113  	case <-t.Halt.ReqStopChan():
   114  	}
   115  }
   116  
   117  func (t *IdleTimer) LastOKLastStartAndMonoNow() (lastOK, lastStart, mnow int64) {
   118  	lastOK = atomic.LoadInt64(&t.lastOK)
   119  	lastStart = atomic.LoadInt64(&t.lastStart)
   120  	mnow = monoNow()
   121  	return
   122  }
   123  
   124  func (t *IdleTimer) BeginAttempt() {
   125  	atomic.StoreInt64(&t.lastStart, monoNow()) // Reset
   126  }
   127  
   128  // Reset stores the current monotonic timestamp
   129  // internally, effectively reseting to zero the value
   130  // returned from an immediate next call to NanosecSince().
   131  //
   132  // AttemptOK() only ever applies to reads now. Writes
   133  // lie: they return nil errors when the connection is down.
   134  //
   135  func (t *IdleTimer) AttemptOK() {
   136  
   137  	// shutdown oneshot?
   138  	// NB we don't support write deadlines now, and
   139  	// never supported having different write and read
   140  	// deadlines, which would need two separate idle timers.
   141  	if atomic.LoadInt32(&t.isOneshot) != 0 {
   142  		t.Halt.RequestStop()
   143  		select {
   144  		case <-t.Halt.DoneChan():
   145  		case <-time.After(10 * time.Second):
   146  			panic("deadlocked during IdleTimer oneshut shutdown")
   147  		}
   148  		return
   149  	}
   150  
   151  	mnow := monoNow()
   152  	atomic.StoreInt64(&t.lastOK, mnow)
   153  	return
   154  }
   155  
   156  // IdleStatus returns three monotonic timestamps.
   157  //
   158  //  * lastStart is the last time BeginAttempt() was called.
   159  //
   160  //  * lastOK is the last time AttemptOK() was called.
   161  //
   162  //  * mnow is the current monotonic timestamp.
   163  //
   164  // Note that lastStart == -1 means there has been no
   165  // BeginAttempt() call started since we set the idle timeout. In
   166  // this case an idle timeout determination may not be appropriate
   167  // because has been no Read attempted since then.
   168  //
   169  // * todur returns the duration in nanoseconds of any timeout
   170  //   that has been set.
   171  //
   172  // * timedout returns true if it appears a Read attempt
   173  //   has timed out before finishing successfully. Note
   174  //   that the Read may have returned with an error and
   175  //   may not be currently active.
   176  //
   177  func (t *IdleTimer) IdleStatus() (lastStart, lastOK, mnow, todur int64, timedout bool) {
   178  	mnow = monoNow()
   179  	lastOK = atomic.LoadInt64(&t.lastOK)
   180  	lastStart = atomic.LoadInt64(&t.lastStart)
   181  	todur = atomic.LoadInt64(&t.atomicdur)
   182  
   183  	if todur <= 0 || lastStart <= 0 || lastOK >= lastStart {
   184  		// no timeout set or no Reads attempted, don't timeout
   185  		return
   186  	}
   187  	// INVAR: lastStart > 0
   188  	// INVAR: lastStart > lastOK
   189  	since := mnow - lastStart
   190  	if since > todur {
   191  		timedout = true
   192  	}
   193  	return
   194  }
   195  
   196  // SetIdleTimeout stores a new idle timeout duration. This
   197  // activates the IdleTimer if dur > 0. Set dur of 0
   198  // to disable the IdleTimer. A disabled IdleTimer
   199  // always returns false from TimedOut().
   200  //
   201  // This is the main API for IdleTimer. Most users will
   202  // only need to use this call.
   203  //
   204  func (t *IdleTimer) SetIdleTimeout(dur time.Duration) error {
   205  	tk := newSetTimeoutTicket(dur)
   206  	select {
   207  	case t.setIdleTimeoutCh <- tk:
   208  	case <-t.Halt.ReqStopChan():
   209  	}
   210  	select {
   211  	case <-tk.done:
   212  	case <-t.Halt.ReqStopChan():
   213  	}
   214  	return nil
   215  }
   216  
   217  func (t *IdleTimer) SetOneshotIdleTimeout(dur time.Duration) {
   218  	atomic.StoreInt32(&t.isOneshot, 1)
   219  	t.SetIdleTimeout(dur)
   220  }
   221  
   222  // GetIdleTimeout returns the current idle timeout duration in use.
   223  // It will return 0 if timeouts are disabled.
   224  func (t *IdleTimer) GetIdleTimeout() (dur time.Duration) {
   225  	select {
   226  	case dur = <-t.getIdleTimeoutCh:
   227  	case <-t.Halt.ReqStopChan():
   228  	}
   229  	return
   230  }
   231  
   232  func (t *IdleTimer) Stop() {
   233  	t.Halt.RequestStop()
   234  	select {
   235  	case <-t.Halt.DoneChan():
   236  	case <-time.After(10 * time.Second):
   237  		panic("IdleTimer.Stop() problem! t.Halt.DoneChan() not received  after 10sec! serious problem")
   238  	}
   239  }
   240  
   241  type setTimeoutTicket struct {
   242  	newdur time.Duration
   243  	done   chan struct{}
   244  }
   245  
   246  func newSetTimeoutTicket(dur time.Duration) *setTimeoutTicket {
   247  	return &setTimeoutTicket{
   248  		newdur: dur,
   249  		done:   make(chan struct{}),
   250  	}
   251  }
   252  
   253  const factor = 10
   254  
   255  func (t *IdleTimer) backgroundStart(dur time.Duration) {
   256  	//pp("IdleTimer.backgroundStart(dur=%v) called.", dur)
   257  	atomic.StoreInt64(&t.atomicdur, int64(dur))
   258  	go func() {
   259  		var heartbeat *time.Ticker
   260  		var heartch <-chan time.Time
   261  		if dur > 0 {
   262  			// we've got to sample at above niquist
   263  			// in order to have a chance of responding
   264  			// quickly to timeouts of dur length. Theoretically
   265  			// dur/2 suffices, but sooner is better so
   266  			// we go with dur/factor. This also allows for
   267  			// some play/some slop in the sampling, which
   268  			// we empirically observe.
   269  			heartbeat = time.NewTicker(dur / factor)
   270  			heartch = heartbeat.C
   271  		}
   272  		defer func() {
   273  			if heartbeat != nil {
   274  				heartbeat.Stop() // allow GC
   275  			}
   276  			t.Halt.MarkDone()
   277  		}()
   278  		for {
   279  			select {
   280  			case <-t.Halt.ReqStopChan():
   281  				return
   282  
   283  			case t.TimedOut <- t.timeOutRaised:
   284  				continue
   285  
   286  			case f := <-t.setCallback:
   287  				t.timeoutCallback = []func(){f.onTimeout}
   288  
   289  			case f := <-t.addCallback:
   290  				t.timeoutCallback = append(t.timeoutCallback, f.onTimeout)
   291  
   292  			case t.getIdleTimeoutCh <- dur:
   293  				continue
   294  
   295  			case tk := <-t.setIdleTimeoutCh:
   296  				/* change state, maybe */
   297  				t.timeOutRaised = ""
   298  				// lastStart == -1 means there has been no
   299  				// Read started since we set the idle timeout.
   300  				atomic.StoreInt64(&t.lastStart, -1) // Reset
   301  
   302  				if dur > 0 {
   303  					// timeouts active currently
   304  					if tk.newdur == dur {
   305  						close(tk.done)
   306  						continue
   307  					}
   308  					if tk.newdur <= 0 {
   309  						// stopping timeouts
   310  						if heartbeat != nil {
   311  							heartbeat.Stop() // allow GC
   312  						}
   313  						dur = tk.newdur
   314  						atomic.StoreInt64(&t.atomicdur, int64(dur))
   315  
   316  						heartbeat = nil
   317  						heartch = nil
   318  						close(tk.done)
   319  						continue
   320  					}
   321  					// changing an active timeout dur
   322  					if heartbeat != nil {
   323  						heartbeat.Stop() // allow GC
   324  					}
   325  					dur = tk.newdur
   326  					atomic.StoreInt64(&t.atomicdur, int64(dur))
   327  
   328  					heartbeat = time.NewTicker(dur / factor)
   329  					heartch = heartbeat.C
   330  					atomic.StoreInt64(&t.lastStart, -1) // Reset
   331  					close(tk.done)
   332  					continue
   333  				} else {
   334  					// heartbeats not currently active
   335  					if tk.newdur <= 0 {
   336  						dur = 0
   337  						atomic.StoreInt64(&t.atomicdur, int64(dur))
   338  
   339  						// staying inactive
   340  						close(tk.done)
   341  						continue
   342  					}
   343  					// heartbeats activating
   344  					dur = tk.newdur
   345  					atomic.StoreInt64(&t.atomicdur, int64(dur))
   346  
   347  					heartbeat = time.NewTicker(dur / factor)
   348  					heartch = heartbeat.C
   349  					atomic.StoreInt64(&t.lastStart, -1) // Reset
   350  					close(tk.done)
   351  					continue
   352  				}
   353  
   354  			case <-heartch:
   355  				if dur == 0 {
   356  					panic("should be impossible to get heartbeat.C on dur == 0")
   357  				}
   358  				lastStart, lastOK, mnow, udur, isTimeout := t.IdleStatus()
   359  				_ = lastOK
   360  				since := mnow - lastStart
   361  				if isTimeout {
   362  					//pp("timing out at %v, in %p! since=%v  dur=%v, exceed=%v. lastOK=%v, waking %v callbacks", time.Now(), t, since, udur, since-udur, lastOK, len(t.timeoutCallback))
   363  
   364  					/* change state */
   365  					t.timeOutRaised = fmt.Sprintf("timing out dur='%v' at %v, in %p! "+
   366  						"since=%v  dur=%v, exceed=%v.",
   367  						dur, time.Now(), t, since, udur, since-udur)
   368  
   369  					// After firing, disable until reactivated.
   370  					// Still must be a ticker and not a one-shot because it may take
   371  					// many, many heartbeats before a timeout, if one happens
   372  					// at all.
   373  					if heartbeat != nil {
   374  						heartbeat.Stop() // allow GC
   375  					}
   376  					heartbeat = nil
   377  					heartch = nil
   378  					if len(t.timeoutCallback) == 0 {
   379  						panic("IdleTimer.timeoutCallback was never set! call t.addTimeoutCallback() first")
   380  					}
   381  					// our caller may be holding locks...
   382  					// and timeoutCallback will want locks...
   383  					// so unless we start timeoutCallback() on its
   384  					// own goroutine, we are likely to deadlock.
   385  					for _, f := range t.timeoutCallback {
   386  						//p("idle.go: timeoutCallback happening")
   387  						go f()
   388  					}
   389  				}
   390  			}
   391  		}
   392  	}()
   393  }