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 }