Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: check for available port before start server (fix #1476, fix #3011) #3039

Merged
merged 1 commit into from
Jun 15, 2018
Merged

fix: check for available port before start server (fix #1476, fix #3011) #3039

merged 1 commit into from
Jun 15, 2018

Conversation

lusarz
Copy link
Contributor

@lusarz lusarz commented Jun 11, 2018

  • create NetUtils with isPortAvailable and getAvailablePort methods
  • use NetUtils to search for available port before starting server and build middlewares
  • remove handler for EADDRINUSE error - with NetUtils.getAvailablePort we'll know that port is available for sure, so this error would no raise

lib/server.js Outdated
})
})
.catch((error) => {
this.log.error('Front-end script compile failed with error: ' + error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using log4js for fatal errors is insufficient: the message gives no stack trace pointing to the problem.
How about adding console.error('Front-end script compile failed with error: \n', error) (note comma).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

lib/server.js Outdated
throw e
}
})
webServer.on('error', (e) => { throw e })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a reader I have to ponder what will happen here. Maybe we can have a dieOnError(err) function that calls log4js, console.error and the process exit dance, then use it here and the compile fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely agree, but I'm not sure this pull request is right place to do it. It may require more investigation.

const server = net.createServer()

server.unref()
server.on('error', () => resolve(false))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we have to check EADDRINUSE, EACCES codes, otherwise reject the promise and terminate karma:

.on('error', function(e) {
    if (e.code != 'EADDRINUSE' && e.code!='EACCES'){
        return reject(e);
    }
    resolve(false)

Copy link
Contributor Author

@lusarz lusarz Jun 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean any specific situation when such check would be relevant ? I'm not aware what else kind of errors we may expect here - I assumed it is ok to check next port no matter what kind of error occurs here.

Probably it would be good to limit number of checks. What do you think ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an example you could specify invalid listenAddress in karma.config: listenAddress: '123'. It should fail with EADDRNOTAVAIL error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I suppose it would be safer to limit checks using known errors related to port usage.

Copy link
Contributor Author

@lusarz lusarz Jun 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sergei-startsev
I've updated pull request with your suggestion. What do you think about current solution ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lusarz thanks for the updates, I've left a few more comments that might be useful.

server.unref()
server.on('error', () => resolve(false))

server.listen({ port }, () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to know whether a process is listening on a port, not just whether the port was available. These aren't exactly the same thing. I believe you also have to specify the host to prevent EACCES:

 server.listen(port, '0.0.0.0', ()=>{...});

Copy link
Contributor Author

@lusarz lusarz Jun 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good point with host - I've updated pull request.

I think isPortAvailable function actually checks whether other process is listening on a port. isPortAvailable try to run some basic server, if everything is ok it close it and resolves with true.

It seems to be enough, but I may miss something.

getAvailablePort (port, listenAddress) {
return NetUtils.isPortAvailable(port, listenAddress).then((available) => {
if (available) return Promise.resolve(port)
else return NetUtils.getAvailablePort(port + 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If config.port starts out of bounds for ports (> 65535), then we will loop until the test times out somehow. This will appear as a hang. I think we should at least log on the else path. Maybe we should limit the quest to say 100 calls.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnjbarton there will be RangeError for ports start out of bounds:

RangeError: "port" argument must be >= 0 and < 65536

So I think the approach suggested above will also work properly here:

if (e.code != 'EADDRINUSE' && e.code!='EACCES'){
    return reject(e);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok great. (I'm unclear on the state of the code here so I'll let you do the merge when ready)

lib/server.js Outdated
@@ -47,6 +48,12 @@ function createSocketIoServer (webServer, executor, config) {
return server
}

function dieOnError (error) {
console.error(error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be this.log.error instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnjbarton suggested here:
#3039 (comment)

to use console.error to log fatal errors. I think it's ok.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm... I suppose this.log.error was initially used here https://github.com/karma-runner/karma/pull/3039/files/193ccdd1147b58ee2117322a105155aea064827e#diff-c945a46d13b34fcaff544d966cffcabaL157

If we would like to replace this.log.error by console.error for fatal errors, it should be consistent across the module, see https://github.com/karma-runner/karma/pull/3039/files/193ccdd1147b58ee2117322a105155aea064827e#diff-c945a46d13b34fcaff544d966cffcabaR160

Otherwise I would leave the existing behavior in that PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used dieOnError for that case now. Now use of console.error for fatal errors is consistent in this module.

lib/server.js Outdated
this.log.error('Front-end script compile failed with error: ' + error)
}
if (this.loadErrors.length > 0) {
this.log.error('Found %d load error%s', this.loadErrors.length, this.loadErrors.length === 1 ? '' : 's')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could dieOnError be used here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

lib/server.js Outdated
BundleUtils.bundleResourceIfNotExist('client/main.js', 'static/karma.js'),
BundleUtils.bundleResourceIfNotExist('context/main.js', 'static/context.js')
])
.then(() => NetUtils.getAvailablePort(this.get('config.port'), this.get('config.listenAddress')))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it reasonable to get config and reuse it below instead of getting it each time?

const config = this.get('config')
...
.then(() => NetUtils.getAvailablePort(config.port, config.listenAddress))
...
config.port = port

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated


server.unref()
server.on('error', (err) => {
server.close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it's redundant

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure ? I see in nodejs documentation (a bit above https://nodejs.org/api/net.html#net_server_listen_handle_backlog_callback) that author invoke server.close() inside server.on('error', (e) => {}) - to be honest I don't know why and whether it's necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://nodejs.org/api/net.html#net_event_error
well, it seems reasonable to leave it

},

getAvailablePort (port, listenAddress, maxAttempts = 10) {
if (!maxAttempts) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you can omit this check since you've added err.code === 'EADDRINUSE' || err.code === 'EACCES' check. 10 attempts aren't enough if you have hundreds test suites and you would like to parallelize them on multi-core machine

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, RangeError should save us here. I'm gonna remove this maxAttempts check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


const NetUtils = require('../../../lib/utils/net-utils')
const connect = require('connect')
const http = require('http')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might use net server, it's not so heavy

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, fixed

Copy link
Contributor

@sergei-startsev sergei-startsev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left a couple of minor comments, otherwise looks good to me.


const NetUtils = require('../../../lib/utils/net-utils')
const connect = require('connect')
const http = require('net')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const net = require('net')

.then((available) => {
if (available) {
return port
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else is redundant

@lusarz lusarz merged commit a19b8d4 into karma-runner:master Jun 15, 2018
@lusarz lusarz deleted the find-available-port-before-start-karma branch October 11, 2018 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants