-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
fix: check for available port before start server (fix #1476, fix #3011) #3039
Conversation
lib/server.js
Outdated
}) | ||
}) | ||
.catch((error) => { | ||
this.log.error('Front-end script compile failed with error: ' + error) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 }) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
lib/utils/net-utils.js
Outdated
const server = net.createServer() | ||
|
||
server.unref() | ||
server.on('error', () => resolve(false)) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
lib/utils/net-utils.js
Outdated
server.unref() | ||
server.on('error', () => resolve(false)) | ||
|
||
server.listen({ port }, () => { |
There was a problem hiding this comment.
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', ()=>{...});
There was a problem hiding this comment.
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.
lib/utils/net-utils.js
Outdated
getAvailablePort (port, listenAddress) { | ||
return NetUtils.isPortAvailable(port, listenAddress).then((available) => { | ||
if (available) return Promise.resolve(port) | ||
else return NetUtils.getAvailablePort(port + 1) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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);
}
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'))) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
lib/utils/net-utils.js
Outdated
}, | ||
|
||
getAvailablePort (port, listenAddress, maxAttempts = 10) { | ||
if (!maxAttempts) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
test/unit/utils/net-utils.spec.js
Outdated
|
||
const NetUtils = require('../../../lib/utils/net-utils') | ||
const connect = require('connect') | ||
const http = require('http') |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, fixed
There was a problem hiding this 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.
test/unit/utils/net-utils.spec.js
Outdated
|
||
const NetUtils = require('../../../lib/utils/net-utils') | ||
const connect = require('connect') | ||
const http = require('net') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const net = require('net')
lib/utils/net-utils.js
Outdated
.then((available) => { | ||
if (available) { | ||
return port | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
else
is redundant
isPortAvailable
andgetAvailablePort
methodsEADDRINUSE
error - withNetUtils.getAvailablePort
we'll know that port is available for sure, so this error would no raise