Skip to content

Commit

Permalink
Feature: Full support of unstable network connection and better handl…
Browse files Browse the repository at this point in the history
…ing of issues with Stapler.

Build Monitor:
* no longer displays an error dialog when a connection error happens, but instead tries to re-connect and notifies the user about progress in a much more subtle way
* can handle internal Jenkins errors such NullPointerException incorrectly returned by Stapler as "200 OK" responses (more details can be found at https://issues.jenkins-ci.org/browse/JENKINS-21132)

Resolves #33 and closes #36
  • Loading branch information
jan-molak committed Dec 23, 2013
1 parent 2f392ca commit 9fa8e54
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 70 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>build-monitor-plugin</artifactId>
<version>1.2-SNAPSHOT</version>
<version>1.3-SNAPSHOT</version>
<packaging>hpi</packaging>

<name>Build Monitor View</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
</j:choose>

<footer>
Brought to you by <a href="http://smartcodeltd.co.uk" rel="external">Jan Molak</a>
<notifier />
<span class="plugin-author">Brought to you by <a href="http://smartcodeltd.co.uk" rel="external">Jan Molak</a></span>
</footer>
</div>

Expand Down
97 changes: 74 additions & 23 deletions src/main/webapp/scripts/controllers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,109 @@
angular.
module('buildMonitor.controllers', [ 'buildMonitor.services', 'buildMonitor.cron', 'uiSlider', 'jenkins']).

controller('JobViews', ['$scope', '$rootScope', 'proxy', 'cookieJar', 'every', 'connectionErrorHandler',
function ($scope, $rootScope, proxy, cookieJar, every, connectionErrorHandler) {
var handleErrorAndDecideOnNext = connectionErrorHandler.handleErrorAndNotify,
fetchJobViews = proxy.buildMonitor.fetchJobViews;
controller('JobViews', ['$scope', '$rootScope', 'proxy', 'cookieJar', 'every', 'connectivityStrategist',
function ($scope, $rootScope, proxy, cookieJar, every, connectivityStrategist) {
var tryToRecover = connectivityStrategist.decideOnStrategy,
fetchJobViews = proxy.buildMonitor.fetchJobViews;

$scope.jobs = {};
$scope.jobs = {};

every(5000, function (step) {
every(5000, function () {

fetchJobViews().then(function (response) {
return fetchJobViews().then(function (response) {

$scope.jobs = response.data.jobs
step.resolve();

}, handleErrorAndDecideOnNext(step));
$rootScope.$broadcast('jenkins:data-fetched', {});

}, tryToRecover());
});
}]).

service('connectionErrorHandler', ['$rootScope',
function ($rootScope) {
this.handleErrorAndNotify = function (deferred) {
service('connectivityStrategist', ['$rootScope', '$q', 'counter',
function ($rootScope, $q, counter) {

var lostConnectionsCount = counter;

this.resetErrorCounter = function () {
if (lostConnectionsCount.value() > 0) {
$rootScope.$broadcast("jenkins:connection-reestablished", {});
}

lostConnectionsCount.reset();
}

this.decideOnStrategy = function () {

function handleLostConnection(error) {
deferred.resolve();
lostConnectionsCount.increase();

$rootScope.$broadcast("jenkins:connection-lost", error);

return $q.when(error);
}

function handleJenkinsRestart(error) {
deferred.reject();
$rootScope.$broadcast("jenkins:restarted", error);
return $q.reject(error);
}

function handleInternalJenkins(error) {
$rootScope.$broadcast("jenkins:internal-error", error);
return $q.reject(error);
}

function handleUnknown(error) {
deferred.reject();
$rootScope.$broadcast("jenkins:unknown-communication-error", error);
return $q.reject(error);
}

return function (error) {
switch (error.status) {
case 0: handleLostConnection(error); break;
case 404: handleJenkinsRestart(error); break;
default: handleUnknown(error); break;
case 0: return handleLostConnection(error);
case 404: return handleJenkinsRestart(error);
case 500: return handleInternalJenkins(error);
default: return handleUnknown(error);
}
}
}
}]).

run(['$rootScope', '$window', '$log', 'notifyUser',
function ($rootScope, $window, $log, notifyUser) {
$rootScope.$on('jenkins:connection-lost', function (event, error) {
// todo: notify the user about the problem and what we're doing in order to resolve it
$log.info('Connection with Jenkins mother ship is lost. I\'ll try to reconnect in a couple of seconds and see if we have more luck...');
directive('notifier', ['$timeout', function ($timeout) {
return {
restrict: 'E',
controller: function ($scope) {
$scope.message = '';

$scope.$on('jenkins:connection-lost', function () {
$scope.message = 'Communication with Jenkins mother ship is lost. Trying to reconnect...';
});

$scope.$on('jenkins:connection-reestablished', function () {
$scope.message = "... and we're back online, yay! :-)";

$timeout(function () {
$scope.message = "";
}, 3000);
});
},
replace: true,
template: '<span class="notifier" ' +
'ng-show="message"' +
'ng-animate="\'fade\'">' +
'{{ message }}' +
"</span>\n"
}
}]).

run(['$rootScope', '$window', '$log', 'notifyUser', 'connectivityStrategist',
function ($rootScope, $window, $log, notifyUser, connectivityStrategist) {
$rootScope.$on('jenkins:data-fetched', function (event) {
connectivityStrategist.resetErrorCounter();
});

$rootScope.$on('jenkins:internal-error', function (event, error) {
notifyUser.aboutInternalJenkins(error);
});

$rootScope.$on('jenkins:restarted', function (event, error) {
Expand Down
33 changes: 14 additions & 19 deletions src/main/webapp/scripts/cron.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,25 @@ angular.
function ($rootScope, $window, $q, $timeout) {

function every(interval, command) {
var applyRootScope = function() {
var isDefined = angular.isDefined,
isFunction = angular.isFunction,
isDeferred = function (result) {
return (isDefined(result) && isFunction(result.then));
},
applyRootScope = function () {
$rootScope.$$phase || $rootScope.$apply();
};

function synchronous(command) {
command();
$timeout(step, interval);
}

function asynchronous(command) {
var deferred = $q.defer(),
promise = deferred.promise;
function step() {
var result = command();

promise.then(function() {
if (isDeferred(result)) {
result.then(function () {
$timeout(step, interval);
})
} else {
$timeout(step, interval);
});

command(deferred);
}

function step() {
(command.length == 0)
? synchronous(command)
: asynchronous(command);
}

applyRootScope();
}
Expand Down
34 changes: 31 additions & 3 deletions src/main/webapp/scripts/jenkins.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

angular.module('jenkins', []).

constant('STAPLER_CONTENT_TYPE', 'application/x-stapler-method-invocation;charset=UTF-8').

provider('proxy', function() {
this.bindings = {};

Expand Down Expand Up @@ -32,7 +34,7 @@ angular.module('jenkins', []).
};
}).

factory('proxyFrom', ['$http', 'stringified', function($http, stringified) {
factory('proxyFrom', ['$http', 'stringified', 'STAPLER_CONTENT_TYPE', function($http, stringified, STAPLER_CONTENT_TYPE) {
return function(binding) {
var url = binding.url + '/',
proxy = {};
Expand All @@ -46,7 +48,7 @@ angular.module('jenkins', []).
method: 'POST',
data: stringified(parameters),
headers: {
'Content-Type': 'application/x-stapler-method-invocation;charset=UTF-8',
'Content-Type': STAPLER_CONTENT_TYPE,
'Crumb': binding.crumb
}
});
Expand All @@ -55,4 +57,30 @@ angular.module('jenkins', []).

return proxy;
}
}]);
}]).

factory('responseCodeStandardsIntroducer', ['$q', 'STAPLER_CONTENT_TYPE', function($q, STAPLER_CONTENT_TYPE) {
function isAStapler(response) {
return (response.config.headers
&& response.config.headers['Content-Type'] === STAPLER_CONTENT_TYPE);
}

return {
'response': function(response) {
if (isAStapler(response) && response.data.stackTrace) {
// this is required to patch the incorrect behaviour of Stapler
// see https://issues.jenkins-ci.org/browse/JENKINS-21132
var augmentedResponse = angular.copy(response);
augmentedResponse.status = 500;

return $q.reject(augmentedResponse);
}

return response;
}
};
}]).

config(function ($httpProvider) {
$httpProvider.interceptors.push('responseCodeStandardsIntroducer');
});
47 changes: 46 additions & 1 deletion src/main/webapp/scripts/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,39 @@ angular.
}).open().then();
}

this.aboutInternalJenkins = function(error) {
function stackTraceOf(error) {
var stackTrace = "";

if (! (error.data && angular.isArray(error.data.stackTrace))) {
return stackTrace;
}

angular.forEach(error.data.stackTrace, function(entry) {
stackTrace += "at " + entry.className + "(" + entry.fileName + ":" + entry.lineNumber + ")\n";
});

return stackTrace;
}

$dialog.dialog({
templateUrl: 'template/dialog/internal-jenkins-error.html',
controller: function($scope, dialog, model) {
$scope.error = model.error;
$scope.stackTrace = model.stackTrace;
},
resolve: { model: function() {
return {
error: JSON.stringify(error.data, undefined, 2),
stackTrace: stackTraceOf(error)
};
}},
keyboard: false,
backdrop: true,
backdropClick: false
}).open().then();
}

this.about = function (problemStatus) {

var title = "Sorry to bother you, but there is a slight issue ...";
Expand Down Expand Up @@ -110,4 +143,16 @@ angular.

return hash;
}
});
}).


factory('counter', [function() {

var value = 0;

return {
reset: function() { value = 0; },
increase: function() { ++value; },
value: function() { return value; }
}
}]);
15 changes: 15 additions & 0 deletions src/main/webapp/scripts/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,19 @@ angular.module("buildMonitor.templates", []).run(["$templateCache", function($te
" </p>\n" +
"</div>\n" +
"");

$templateCache.put("template/dialog/internal-jenkins-error.html",
"<div class=\"modal-header\">\n" +
" <h1>Sorry to bother you, but Jenkins is having a problem :-(</h1>\n" +
"</div>\n" +
"<div class=\"modal-body\">\n" +
" <p>Instead of an expected response I received the following, which usually means an internal Jenkins error:</p>" +
" <textarea rows='5'>{{ error }}</textarea>\n" +
" <div ng-show='{{ stackTrace.length }}'>\n" +
" <p>This translates to the following stack trace:</p>\n" +
" <textarea rows='5'>{{ stackTrace }}</textarea>\n" +
" </div>\n" +
" <p>Before <a href='https://issues.jenkins-ci.org/secure/Dashboard.jspa'>reporting a bug in Jenkins</a> it's usually worth trying to troubleshoot it using the information from your <a href='https://wiki.jenkins-ci.org/display/JENKINS/Logging'>Jenkins Error Log</a>.</p>\n" +
"</div>\n" +
"");
}]);
Loading

0 comments on commit 9fa8e54

Please sign in to comment.