/** * An element which displays and auto-updates the logs from a build. */ angular.module('quay').directive('buildLogsView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/build-logs-view.html', replace: false, transclude: false, restrict: 'C', scope: { 'build': '=build', 'useTimestamps': '=useTimestamps', 'buildUpdated': '&buildUpdated' }, controller: function($scope, $element, $interval, $sanitize, ansi2html, AngularViewArray, AngularPollChannel, ApiService, Restangular) { var result = $element.find('#copyButton').clipboardCopy(); if (!result) { $element.find('#copyButton').hide(); } $scope.logEntries = null; $scope.currentParentEntry = null; $scope.logStartIndex = 0; $scope.buildLogsText = ''; $scope.currentBuild = null; var pollChannel = null; var appendToTextLog = function(type, message) { if (type == 'phase') { text = 'Starting phase: ' + message + '\n'; } else { text = message + '\n'; } $scope.buildLogsText += text.replace(new RegExp("\\033\\[[^m]+m"), ''); }; var processLogs = function(logs, startIndex, endIndex) { if (!$scope.logEntries) { $scope.logEntries = []; } // If the start index given is less than that requested, then we've received a larger // pool of logs, and we need to only consider the new ones. if (startIndex < $scope.logStartIndex) { logs = logs.slice($scope.logStartIndex - startIndex); } for (var i = 0; i < logs.length; ++i) { var entry = logs[i]; var type = entry['type'] || 'entry'; if (type == 'command' || type == 'phase' || type == 'error') { entry['logs'] = AngularViewArray.create(); entry['index'] = $scope.logStartIndex + i; $scope.logEntries.push(entry); $scope.currentParentEntry = entry; } else if ($scope.currentParentEntry) { $scope.currentParentEntry['logs'].push(entry); } appendToTextLog(type, entry['message']); } return endIndex; }; var getBuildStatusAndLogs = function(build, callback) { var params = { 'repository': build.repository.namespace + '/' + build.repository.name, 'build_uuid': build.id }; ApiService.getRepoBuildStatus(null, params, true).then(function(resp) { if (resp.id != $scope.build.id) { callback(false); return; } // Call the build updated handler. $scope.buildUpdated({'build': resp}); // Save the current build. $scope.currentBuild = resp; // Load the updated logs for the build. var options = { 'start': $scope.logStartIndex }; ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) { // Process the logs we've received. $scope.logStartIndex = processLogs(resp['logs'], resp['start'], resp['total']); // If the build status is an error, open the last two log entries. var currentBuild = $scope.currentBuild; if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) { var openLogEntries = function(entry) { if (entry.logs) { entry.logs.setVisible(true); } }; openLogEntries($scope.logEntries[$scope.logEntries.length - 2]); openLogEntries($scope.logEntries[$scope.logEntries.length - 1]); } // If the build phase is an error or a complete, then we mark the channel // as closed. callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete'); }, function() { callback(false); }); }, function() { callback(false); }); }; var startWatching = function(build) { // Create a new channel for polling the build status and logs. var conductStatusAndLogRequest = function(callback) { getBuildStatusAndLogs(build, callback); }; pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */); pollChannel.start(); }; var stopWatching = function() { if (pollChannel) { pollChannel.stop(); pollChannel = null; } }; $scope.$watch('useTimestamps', function() { if (!$scope.logEntries) { return; } $scope.logEntries = $scope.logEntries.slice(); }); $scope.$watch('build', function(build) { if (build) { startWatching(build); } else { stopWatching(); } }); $scope.hasLogs = function(container) { return container.logs.hasEntries; }; $scope.formatDatetime = function(datetimeString) { var dt = new Date(datetimeString); return dt.toLocaleString(); } $scope.processANSI = function(message, container) { var filter = container.logs._filter = (container.logs._filter || ansi2html.create()); // Note: order is important here. var setup = filter.getSetupHtml(); var stream = filter.addInputToStream(message); var teardown = filter.getTeardownHtml(); return setup + stream + teardown; }; } }; return directiveDefinitionObject; });