Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Implement Jenkins#consoleStream #11

Merged
merged 1 commit into from

2 participants

@5long

This branch is just #10 and:

  • rebased against master branch
  • squashed into a single commit
@cliffano
Owner

Looks good at a glance. I'm going to test it against my common usage scenarios first.

@cliffano cliffano merged commit 298a6f9 into from
@cliffano
Owner

Thanks for the contribution Whyme. This change has been included in v0.1.2 .

@cliffano
Owner

Hmm, I just tested the new console handling a bit more and I noticed that it no longer behaves as it was originally designed.

The intention was to simulate console output display on the terminal the same way as the console output is displayed on Jenkins web, i.e. it should progress along the build.

@5long

I'm using nestor console to monitor our deploy job and it works as intended. A moment before I've tested nestor console with a lengthy frontend assets compiling job and it works too. By "it works" I mean nestor console and <jenkins url>/job/<job name>/<job id>/console are printing output as the same pace.

What is the pattern of your job output? Is it a dot matrix of unit test result which doesn't print any \n for a long time? I'm suspecting that the stdout of node is buffered line-wise but I'm not quite sure.

I'll do more testing against more jobs and see if I can reproduce it.

Also, I'm running nodejs 0.8.22, nestor 0.1.3 and Jenkins 1.480.3 LTS.

@5long

Well it turns out that I guessed wrong. process.stdout.write is synced by default when stdout is TTY: http://nodejs.org/api/process.html#process_process_stdout

@cliffano
Owner

Weird, I can't reproduce the problem now. What I saw was tty console output being written without new lines and at a different pace compared to web console output. I used node v0.10.1 .

Don't worry about it. I'll do more testing, but I think it was a false alarm.

Thanks for the confirmation.

@5long 5long deleted the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 31, 2013
  1. @5long
This page is out of date. Refresh to see the latest.
Showing with 67 additions and 36 deletions.
  1. +10 −0 lib/console_stream.js
  2. +33 −11 lib/jenkins.js
  3. +24 −25 test/jenkins.js
View
10 lib/console_stream.js
@@ -0,0 +1,10 @@
+var inherits = require('util').inherits,
+ Stream = require('stream').Stream;
+
+function ConsoleStream() {}
+inherits(ConsoleStream, Stream);
+
+ConsoleStream.prototype.readable = true;
+ConsoleStream.prototype.writable = false;
+
+module.exports = ConsoleStream;
View
44 lib/jenkins.js
@@ -5,7 +5,8 @@ var _ = require('underscore'),
dgram = require('dgram'),
feedparser = require('feedparser'),
request = require('request'),
- xml2js = require('xml2js');
+ xml2js = require('xml2js'),
+ ConsoleStream = require('./console_stream');
/**
* class Jenkins
@@ -72,30 +73,36 @@ Jenkins.prototype.build = function (jobName, params, cb) {
};
/**
- * Display build progress console output.
+ * Read a Jenkins job console output as a readable stream.
* Jenkins uses the following headers:
* - x-more-data to determine whether there are still more content coming through
* - x-text-size to determine the starting point of progressive text output
- * Uses process.stdout.write instead of console.log because console.log adds an empty line.
*
* @param {String} jobName: Jenkins job name
* @param {Object} opts: optional interval in milliseconds
* @param {Function} cb: standard cb(err, result) callback
+ * @return {ConsoleStream} Returns readable stream
*/
-Jenkins.prototype.console = function (jobName, opts, cb) {
+Jenkins.prototype.consoleStream = function(jobName, opts, cb) {
if (!cb) {
- cb = opts;
+ if (typeof opts === 'function') {
+ cb = opts;
+ opts = undefined;
+ } else {
+ cb = function(){};
+ }
}
const INTERVAL = 1000;
var url = this.url + '/job/' + jobName + '/lastBuild/logText/progressiveText',
+ stream = new ConsoleStream(),
self = this;
this.opts.queryStrings = { start: 0 }; // the first chunk
function _success(result, cb) {
if (result.body) {
- console.log(result.body);
+ stream.emit('data', result.body);
}
// stream while there are more data
async.whilst(
@@ -117,15 +124,14 @@ Jenkins.prototype.console = function (jobName, opts, cb) {
} else {
result = _result;
if (_result.body) {
- console.log(_result.body);
+ stream.emit('data', _result.body);
}
- setTimeout(function () {
- cb();
- }, (opts && opts.interval) ? opts.interval : INTERVAL);
+ setTimeout(cb, (opts && opts.interval) ? opts.interval : INTERVAL);
}
});
},
function (err) {
+ stream.emit('end');
cb(err);
}
);
@@ -138,7 +144,23 @@ Jenkins.prototype.console = function (jobName, opts, cb) {
this.opts.handlers[200] = _success;
this.opts.handlers[404] = _notFound;
- bag.http.request('get', url, this.opts, cb);
+ process.nextTick(function() {
+ bag.http.request('get', url, self.opts, cb);
+ });
+
+ return stream;
+};
+
+/**
+ * Display build progress console output.
+ *
+ * @param {String} jobName: Jenkins job name
+ * @param {Object} opts: optional interval in milliseconds
+ * @param {Function} cb: standard cb(err, result) callback
+ */
+Jenkins.prototype.console = function (jobName, opts, cb) {
+ var stream = this.consoleStream(jobName, opts, cb);
+ stream.pipe(process.stdout, {end: false});
};
/**
View
49 test/jenkins.js
@@ -133,7 +133,7 @@ buster.testCase('jenkins - build', {
}
});
-buster.testCase('jenkins - console', {
+buster.testCase('jenkins - consoleStream', {
setUp: function () {
this.mockConsole = this.mock(console);
},
@@ -145,27 +145,27 @@ buster.testCase('jenkins - console', {
};
this.stub(bag, 'http', { request: mockRequest });
var jenkins = new Jenkins('http://localhost:8080');
- jenkins.console('job1', function (err, result) {
+ jenkins.consoleStream('job1', function (err, result) {
assert.equals(err.message, 'Job job1 does not exist');
assert.equals(result, undefined);
done();
});
},
'should display a single console output when there is no more text': function (done) {
- this.mockConsole.expects('log')
- .once().withExactArgs('Job started by Foo');
var mockRequest = function (method, url, opts, cb) {
assert.equals(method, 'get');
assert.equals(url, 'http://localhost:8080/job/job1/lastBuild/logText/progressiveText');
opts.handlers[200]({ statusCode: 200, body: 'Job started by Foo', headers: { 'x-more-data': 'false' } }, cb);
};
this.stub(bag, 'http', { request: mockRequest });
- var jenkins = new Jenkins('http://localhost:8080');
- jenkins.console('job1', function (err, result) {
+ var jenkins = new Jenkins('http://localhost:8080'),
+ read = [];
+ jenkins.consoleStream('job1', function (err, result) {
assert.equals(err, undefined);
assert.equals(result, undefined);
+ assert.equals(read, ['Job started by Foo']);
done();
- });
+ }).on('data', read.push.bind(read));
},
'should not display anything if there is only a single console output with no value (e.g. build step sleeping for several seconds)': function (done) {
this.mockConsole.expects('log').never();
@@ -176,17 +176,15 @@ buster.testCase('jenkins - console', {
};
this.stub(bag, 'http', { request: mockRequest });
var jenkins = new Jenkins('http://localhost:8080');
- jenkins.console('job1', function (err, result) {
+ jenkins.consoleStream('job1', function (err, result) {
assert.equals(err, undefined);
assert.equals(result, undefined);
done();
+ }).on('data', function() {
+ assert(false, "There should be no data");
});
},
'should display console output until there is no more text': function (done) {
- this.mockConsole.expects('log')
- .once().withExactArgs('Console output 1');
- this.mockConsole.expects('log')
- .once().withExactArgs('Console output 2');
// only first request uses bag.http.request
var mockBagRequest = function (method, url, opts, cb) {
assert.equals(method, 'get');
@@ -205,16 +203,16 @@ buster.testCase('jenkins - console', {
headers: { 'x-more-data': 'false', 'x-text-size': 20 }
});
this.stub(bag, 'http', { request: mockBagRequest, proxy: function(url) { return 'http://someproxy'; }});
- var jenkins = new Jenkins('http://localhost:8080');
- jenkins.console('job1', { interval: 1 }, function (err, result) {
+ var jenkins = new Jenkins('http://localhost:8080'),
+ read = [];
+ jenkins.consoleStream('job1', { interval: 1 }, function (err, result) {
assert.equals(err, undefined);
assert.equals(result, undefined);
+ assert.equals(read, ['Console output 1', 'Console output 2']);
done();
- });
+ }).on('data', read.push.bind(read));
},
'should not display chunked console output when result body is undefined': function (done) {
- this.mockConsole.expects('log')
- .once().withExactArgs('Console output 1');
// only first request uses bag.http.request
var mockBagRequest = function (method, url, opts, cb) {
assert.equals(method, 'get');
@@ -232,16 +230,15 @@ buster.testCase('jenkins - console', {
headers: { 'x-more-data': 'false', 'x-text-size': 20 }
});
this.stub(bag, 'http', { request: mockBagRequest, proxy: function () { return undefined; }});
- var jenkins = new Jenkins('http://localhost:8080');
- jenkins.console('job1', { interval: 1 }, function (err, result) {
+ var jenkins = new Jenkins('http://localhost:8080'),
+ read = [];
+ jenkins.consoleStream('job1', { interval: 1 }, function (err, result) {
assert.equals(err, undefined);
assert.equals(result, undefined);
done();
- });
+ }).on('data', read.push.bind(read));
},
'should pass error when chunking console output has an error': function (done) {
- this.mockConsole.expects('log')
- .once().withExactArgs('Console output 1');
// only first request uses bag.http.request
var mockBagRequest = function (method, url, opts, cb) {
assert.equals(method, 'get');
@@ -256,12 +253,14 @@ buster.testCase('jenkins - console', {
var mockRequest = this.mock(request);
mockRequest.expects('get').once().callsArgWith(1, new Error('someerror'));
this.stub(bag, 'http', { request: mockBagRequest, proxy: function () { return undefined; }});
- var jenkins = new Jenkins('http://localhost:8080');
- jenkins.console('job1', { interval: 1 }, function (err, result) {
+ var jenkins = new Jenkins('http://localhost:8080'),
+ read = [];
+ jenkins.consoleStream('job1', { interval: 1 }, function (err, result) {
assert.equals(err.message, 'someerror');
assert.equals(result, undefined);
+ assert.equals(read, ['Console output 1']);
done();
- });
+ }).on('data', read.push.bind(read));
}
});
Something went wrong with that request. Please try again.