Skip to content

Commit

Permalink
Revert "remove timers"
Browse files Browse the repository at this point in the history
This reverts commit b9d65a4.
  • Loading branch information
flotwig committed Jun 11, 2019
1 parent 9c8337e commit 93f15fc
Show file tree
Hide file tree
Showing 5 changed files with 311 additions and 0 deletions.
6 changes: 6 additions & 0 deletions packages/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ require('./lib/util/stdio').makeAsync()
// override tty if we're being forced to
require('./lib/util/tty').override()

// if we are running in electron
// we must hack around busted timers
if (process.versions.electron) {
require('./timers/parent').fix()
}

if (process.env.CY_NET_PROFILE && process.env.CYPRESS_ENV) {
const netProfiler = require('./lib/util/net_profiler')()

Expand Down
5 changes: 5 additions & 0 deletions packages/server/lib/plugins/child/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// if we are running in electron
// we must hack around busted timers
if (process.versions.electron) {
require('../../timers/parent').fix()
}
require('graceful-fs').gracefulify(require('fs'))
require('@packages/coffee/register')
require && require.extensions && delete require.extensions['.litcoffee']
Expand Down
112 changes: 112 additions & 0 deletions packages/server/test/unit/timers_spec.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
require("../spec_helper")

_ = require("lodash")

parent = require("#{root}timers/parent")

describe "timers/parent", ->
context ".fix", ->
beforeEach ->
parent.restore()
@timer = parent.fix()

describe "setTimeout", ->
it "returns timer object", (done) ->
obj = setTimeout(done, 10)

expect(obj.id).to.eq(1)
expect(obj.ref).to.be.a("function")
expect(obj.unref).to.be.a("function")

it "increments timer id", (done) ->
fn = _.after(2, done)

obj1 = setTimeout(fn, 10)
obj2 = setTimeout(fn, 10)

expect(obj2.id).to.eq(2)

it "slices out of queue once cb is invoked", (done) ->
fn = =>
expect(@timer.queue).to.deep.eq({})
done()

setTimeout(fn, 10)

expect(@timer.queue[1].cb).to.eq(fn)

describe "clearTimeout", ->
it "does not explode when passing null", ->
clearTimeout(null)

it "can clear the timeout and prevent the cb from being invoked", (done) ->
fn = =>
done(new Error("should not have been invoked"))

timer = setTimeout(fn, 10)

expect(@timer.queue[1].cb).to.eq(fn)

clearTimeout(timer)

expect(@timer.queue).to.deep.eq({})

setTimeout ->
done()
, 20

describe "setInterval", ->
it "returns timer object", (done) ->
obj = setInterval ->
clearInterval(obj)

done()
, 10

expect(obj.id).to.eq(1)
expect(obj.ref).to.be.a("function")
expect(obj.unref).to.be.a("function")

it "increments timer id", (done) ->
fn = _.after 2, ->
clearInterval(obj1)
clearInterval(obj2)
done()

obj1 = setInterval(fn, 10)
obj2 = setInterval(fn, 10000)

expect(obj2.id).to.eq(2)

it "continuously polls until cleared", (done) ->
poller = _.after 3, =>
clearInterval(t)

setTimeout ->
expect(fn).to.be.calledThrice
done()
, 100

fn = sinon.spy(poller)

t = setInterval(fn, 10)

describe "clearInterval", ->
it "does not explode when passing null", ->
clearInterval(null)

it "can clear the interval and prevent the cb from being invoked", (done) ->
fn = =>
done(new Error("should not have been invoked"))

timer = setInterval(fn, 10)

expect(@timer.queue[1].cb).to.exist

clearInterval(timer)

expect(@timer.queue).to.deep.eq({})

setTimeout ->
done()
, 20
22 changes: 22 additions & 0 deletions packages/server/timers/child.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const log = require('debug')('cypress:server:timers')

process.on('message', (obj = {}) => {
const { id, ms } = obj

log('child received timer id %d', id)

setTimeout(() => {
try {
log('child sending timer id %d', id)

// process.send could throw if
// parent process has already exited
process.send({
id,
ms,
})
} catch (err) {
// eslint-disable no-empty
}
}, ms)
})
166 changes: 166 additions & 0 deletions packages/server/timers/parent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// electron has completely busted timers resulting in
// all kinds of bizarre timeouts and unresponsive UI
// https://github.com/electron/electron/issues/7079
//
// this fixes this problem by replacing all the global
// timers and implementing a lightweight queuing mechanism
// involving a forked process

const cp = require('child_process')
const path = require('path')
const log = require('debug')('cypress:server:timers')

const st = global.setTimeout
const si = global.setInterval
const ct = global.clearTimeout
const ci = global.clearInterval

let child = null

function noop () {}

function restore () {
// restore
global.setTimeout = st
global.setInterval = si
global.clearTimeout = ct
global.clearInterval = ci

if (child) {
child.kill()
}

child = null
}

function fix () {
const queue = {}
let idCounter = 0

function sendAndQueue (id, cb, ms, args) {
// const started = Date.now()
log('queuing timer id %d after %d ms', id, ms)

queue[id] = {
// started,
args,
ms,
cb,
}

child.send({
id,
ms,
})

// return the timer object
return {
id,
ref: noop,
unref: noop,
}
}

function clear (id) {
log('clearing timer id %d from queue %o', id, queue)

delete queue[id]
}

// fork the child process
let child = cp.fork(path.join(__dirname, 'child.js'), [], {
stdio: 'inherit',
})
.on('message', (obj = {}) => {
const { id } = obj

const msg = queue[id]

// if we didn't get a msg
// that means we must have
// cleared the timeout already
if (!msg) {
return
}

const { cb, args } = msg

clear(id)

cb(...args)
})

// In linux apparently the child process is never
// exiting which causes cypress to hang indefinitely.
// It would **SEEM** as if we...
// 1. dont need to manually kill our child process
// because it should naturally exit.
// (but of course it doesn't in linux)
// 2. use our restore function already defined above.
// however when using the restore function above
// the 'child' reference is null. how is it null?
// it makes no sense. there must be a rip in the
// space time continuum, obviously. that or the
// child reference as the rest of the matter of
// the universe has succumbed to entropy.
process.on('exit', () => {
child && child.kill()

restore()
})

global.setTimeout = function (cb, ms, ...args) {
idCounter += 1

return sendAndQueue(idCounter, cb, ms, args)
}

global.clearTimeout = function (timer) {
if (!timer) {
return
}

// return undefined per the spec
clear(timer.id)
}

global.clearInterval = function (timer) {
if (!timer) {
return
}

// return undefined per the spec
clear(timer.id)
}

global.setInterval = function (fn, ms, ...args) {
const permId = idCounter += 1

function cb () {
// we want to immediately poll again
// because our permId was just cleared
// from the queue stack
poll()

fn()
}

function poll () {
return sendAndQueue(permId, cb, ms, args)
}

return poll()
}

return {
child,

queue,
}
}

module.exports = {
restore,

fix,
}

0 comments on commit 93f15fc

Please sign in to comment.