Add @svenfuchs' log reimplementation

This commit is contained in:
Piotr Sarnacki 2013-02-22 16:55:53 +01:00
parent 78dc69b61e
commit 553495c56c
13 changed files with 278 additions and 466 deletions

View File

@ -1,4 +1,3 @@
require 'travis/log'
require 'config/emoij'
@Travis.Helpers =

View File

@ -1,6 +1,5 @@
require 'models/extensions'
require 'models/account'
require 'models/artifact'
require 'models/broadcast'
require 'models/branch'
require 'models/build'
@ -8,6 +7,7 @@ require 'models/commit'
require 'models/event'
require 'models/hook'
require 'models/job'
require 'models/log'
require 'models/repo'
require 'models/sponsor'
require 'models/user'

View File

@ -1,89 +0,0 @@
require 'travis/model'
@Travis.Artifact = Em.Object.extend
version: 1 # used to refresh log on requeue
body: null
isLoaded: false
init: ->
@_super.apply this, arguments
@addObserver 'job.id', @fetchBody
@fetchBody()
@set 'queue', Ember.A([])
@set 'parts', Ember.ArrayProxy.create(content: [])
@addObserver 'body', @fetchWorker
@fetchWorker()
id: (->
@get('job.id')
).property('job.id')
clear: ->
@set('body', '')
@incrementProperty('version')
fetchBody: ->
if jobId = @get('job.id')
@removeObserver 'job.id', @fetchBody
self = this
Travis.ajax.ajax "/jobs/#{jobId}/log.txt?cors_hax=true", 'GET',
dataType: 'text'
contentType: 'text/plain'
success: (data, textStatus, xhr) ->
if xhr.status == 204
logUrl = xhr.getResponseHeader('X-Log-Location')
# For some reason not all browsers can fetch this header
unless logUrl
logUrl = self.s3Url("/jobs/#{jobId}/log.txt")
$.ajax
url: logUrl
type: 'GET'
success: (data) ->
self.fetchedBody(data)
else
self.fetchedBody(data)
s3Url: (path) ->
endpoint = Travis.config.api_endpoint
staging = if endpoint.match(/-staging/) then '-staging' else ''
host = Travis.config.api_endpoint.replace(/^https?:\/\//, '').split('.').slice(-2).join('.')
"https://s3.amazonaws.com/archive#{staging}.#{host}#{path}"
fetchedBody: (body) ->
@set 'body', body
@set 'isLoaded', true
append: (body) ->
if @get('isInitialized')
@get('parts').pushObject body
@set('body', @get('body') + body)
else
@get('queue').pushObject(body)
recordDidLoad: (->
if @get('isLoaded')
if (body = @get 'body') && @get('parts.length') == 0
@get('parts').pushObject body
@set 'isInitialized', true
queue = @get('queue')
if queue.get('length') > 0
@append queue.toArray().join('')
).observes('isLoaded')
fetchWorker: ->
if !@get('workerName') && (body = @get('body'))
line = body.split("\n")[0]
if line && (match = line.match /Using worker: (.*)/)
if worker = match[1]
worker = worker.trim().split(':')[0]
@set('workerName', worker)
@removeObserver 'body', @fetchWorker

View File

@ -18,8 +18,9 @@ require 'travis/model'
build: DS.belongsTo('Travis.Build')
commit: DS.belongsTo('Travis.Commit')
commits: DS.belongsTo('Travis.Commit')
log: ( ->
Travis.Artifact.create(job: this)
Travis.Log.create(job: this)
).property()
repoSlug: (->
@ -39,7 +40,7 @@ require 'travis/model'
).property('state')
clearLog: ->
@get('log').clear() if @get('log.isLoaded')
@get('log').clear()
sponsor: (->
worker = @get('log.workerName')
@ -71,13 +72,13 @@ require 'travis/model'
requeue: ->
Travis.ajax.post '/requests', job_id: @get('id')
appendLog: (text) ->
if log = @get('log')
log.append(text)
appendLog: (part) ->
@get('log').append part
subscribe: ->
if id = @get('id')
Travis.pusher.subscribe "job-#{id}"
return if @get('subscribed')
@set('subscribed', true)
Travis.pusher.subscribe "job-#{@get('id')}"
onStateChange: (->
if @get('state') == 'finished' && Travis.pusher

View File

@ -0,0 +1,68 @@
require 'travis/model'
@Travis.Log = Em.Object.extend
version: 0 # used to refresh log on requeue
isLoaded: false
length: 0
init: ->
@set('parts', Ember.ArrayProxy.create(content: []))
@fetch()
fetch: ->
console.log 'fetch'
handlers =
json: (json) => @loadParts(json['log']['parts'])
text: (text) => @loadText(text)
Travis.Log.Request.create(id: id, handlers: handlers).run() if id = @get('job.id')
clear: ->
@set('parts', Ember.ArrayProxy.create(content: []))
@incrementProperty('version')
append: (part) ->
@get('parts').pushObject(part)
loadParts: (parts) ->
@append(part) for part in parts
@set('isLoaded', true)
loadText: (text) ->
number = -1
@append(number: 0, content: text)
@set('isLoaded', true)
Travis.Log.Request = Em.Object.extend
HEADERS:
accept: 'application/vnd.travis-ci.2+json; chunked=true; version=2, text/plain; version=2'
run: ->
Travis.ajax.ajax "/jobs/#{@id}/log?cors_hax=true", 'GET',
dataType: 'text'
headers: @HEADERS
success: (body, status, xhr) => @handle(body, status, xhr)
handle: (body, status, xhr) ->
if xhr.status == 204
$.ajax(url: @redirectTo(xhr), type: 'GET', success: @handlers.text)
else if @isJson(xhr, body)
@handlers.json(JSON.parse(body))
else
@handlers.text(body)
redirectTo: (xhr) ->
# Firefox can't see the Location header on the xhr response due to the wrong
# status code 204. Should be some redirect code but that doesn't work with CORS.
xhr.getResponseHeader('Location') || @s3Url()
s3Url: ->
endpoint = Travis.config.api_endpoint
staging = if endpoint.match(/-staging/) then '-staging' else ''
host = endpoint.replace(/^https?:\/\//, '').split('.').slice(-2).join('.')
"https://s3.amazonaws.com/archive#{staging}.#{host}#{path}/jobs/#{@id}/log.txt"
isJson: (xhr, body) ->
# Firefox can't see the Content-Type header on the xhr response due to the wrong
# status code 204. Should be some redirect code but that doesn't work with CORS.
type = xhr.getResponseHeader('Content-Type') || ''
type.indexOf('json') > -1 || body.slice(0, 8) == '{"log":{'

View File

@ -52,7 +52,7 @@ Travis.Store = DS.Store.extend
!!@typeMapFor(type).idToCid[id]
receive: (event, data) ->
console.log event, data
#console.log event, data
[name, type] = event.split(':')
mappings = @adapter.get('mappings')
@ -79,8 +79,9 @@ Travis.Store = DS.Store.extend
if event == 'job:log'
if job = @find(Travis.Job, data['job']['id'])
job.appendLog(data['job']['_log'])
data = data.job
job = @find(Travis.Job, data.id)
job.appendLog(number: parseInt(data.number), content: data._log)
else if data[type.singularName()]
@_loadOne(this, type, data)
else if data[type.pluralName()]

View File

@ -46,6 +46,6 @@
{{view Travis.JobsView jobsBinding="build.requiredJobs" required="true"}}
{{view Travis.JobsView jobsBinding="build.allowedFailureJobs"}}
{{else}}
{{view Travis.LogView contextBinding="build.jobs.firstObject"}}
{{view Travis.LogView jobBinding="build.jobs.firstObject"}}
{{/if}}
{{/if}}

View File

@ -1,13 +1,7 @@
{{view.logSubscriber}}
{{#if view.log.isLoaded}}
{{view Travis.PreView logBinding="view.log" logUrlBinding="view.logUrl"}}
{{#if view.job.log.isLoaded}}
{{! this #with + #if is needed because I want to rerender 'pre' when log changes to properly clean it up,
this should probably be refactored to use container view}}
{{#with view.job.log}}
{{view Travis.PreView logBinding="view.context.log" logUrlBinding="view.logUrl"}}
{{/with}}
{{#if sponsor.name}}
{{#if view.job.sponsor.name}}
<p class="sponsor">
{{t builds.messages.sponsored_by}}
<a {{bindAttr href="sponsor.url"}}>{{sponsor.name}}</a>

View File

@ -42,7 +42,7 @@
<dd class="config">{{formatConfig job.config}}</dd>
</dl>
{{view Travis.LogView contextBinding="job"}}}
{{view Travis.LogView jobBinding="job"}}
</div>
{{else}}
<div id="job" class="loading">

View File

@ -21,6 +21,7 @@ require 'views/build'
require 'views/events'
require 'views/flash'
require 'views/job'
require 'views/log'
require 'views/repo'
require 'views/profile'
require 'views/sidebar'

View File

@ -1,4 +1,4 @@
@Travis.reopen
Travis.reopen
JobsView: Travis.View.extend
templateName: 'jobs/list'
buildBinding: 'controller.build'
@ -37,204 +37,3 @@
urlCommitter: (->
Travis.Urls.email(@get('commit.committerEmail'))
).property('commit.committerEmail')
LogView: Travis.View.extend
templateName: 'jobs/log'
logBinding: 'job.log'
plainTextLogUrl: (->
if id = @get('job.log.id')
Travis.Urls.plainTextLog(id)
).property('job.log')
didInsertElement: ->
@_super.apply this, arguments
@tryScrollingToHashLineNumber()
scrollTo: (hash) ->
# and this is even more weird, when changing hash in URL in firefox
# to other value, for example #L10, it actually scrolls just #main
# element... this is probably some CSS issue, I don't have time to
# investigate at the moment
# TODO: fix this
$('#main').scrollTop 0
# weird, html works in chrome, body in firefox
$('html,body').scrollTop $(hash).offset().top
@set 'controller.lineNumberHash', null
lineNumberHashDidChange: (->
@tryScrollingToHashLineNumber()
).observes('controller.lineNumberHash')
tryScrollingToHashLineNumber: ->
if hash = @get 'controller.lineNumberHash'
self = this
checker = ->
return if self.get('isDestroyed')
if $(hash).length
self.scrollTo(hash)
else
setTimeout checker, 100
checker()
click: ->
target = $(event.target)
target.closest('.fold').toggleClass('open')
if target.is('a') && target.attr('id') && target.attr('id').match(/^L\d+$/)
path = target.attr 'href'
Travis.get('router').route(path)
event.stopPropagation()
return false
toTop: () ->
$(window).scrollTop(0)
jobBinding: 'context'
logSubscriber: (->
# for some reason observing context does not work,
# TODO: find out why
job = @get('job')
job.subscribe() if job && !job.get('isFinished')
null
).property('job', 'job.state')
logUrl: (->
repo = @get('job.repo')
item = @get('parentView.currentItem')
if repo && item
event = if item.constructor == Travis.Build
'showBuild'
else
'showJob'
#Travis.get('router').urlForEvent(event, repo, item)
).property('job.repo', 'parentView.currentItem')
PreView: Em.View.extend
templateName: 'jobs/pre'
init: ->
@_super.apply this, arguments
@set 'logManager', Travis.Log.create(target: this)
toggleTailing: ->
Travis.tailing.toggle()
event.preventDefault()
didInsertElement: ->
@_super.apply this, arguments
Ember.run.next this, ->
if @get 'log.isInitialized'
@logDidChange()
willDestroy: ->
@get('logManager').destroy()
@get('log.parts').removeArrayObserver this,
didChange: 'logContentsDidChange'
willChange: 'logContentsWillChange'
version: (->
@rerender()
@set 'logManager', Travis.Log.create(target: this)
).observes('log.version')
logDidChange: (->
if @get('log.isInitialized') && @state == 'inDOM'
@attachLogObservers()
).observes('log', 'log.isInitialized')
attachLogObservers: ->
return if @get('logPartsObserversAttached') == Ember.guidFor(@get('log'))
@set 'logPartsObserversAttached', Ember.guidFor(@get('log'))
Ember.run.next this, ->
@get('logManager').append @get('log.parts')
@get('log.parts').addArrayObserver this,
didChange: 'logContentsDidChange'
willChange: 'logContentsWillChange'
logContentsDidChange: (lines, index, removedCount, addedCount) ->
addedLines = lines.slice(index, index + addedCount)
@get('logManager').append addedLines
logContentsWillChange: (-> )
appendLog: (payloads) ->
url = @get('logUrl')
leftOut = []
cut = false
fragment = document.createDocumentFragment()
# TODO: refactor this loop, it's getting messy
for payload in payloads
line = payload.content
number = payload.number
if payload.logWasCut
cut = true
else
unless payload.append
pathWithNumber = "#{url}#L#{number}"
p = document.createElement('p')
p.innerHTML = "<a href=\"#{pathWithNumber}\" id=\"L#{number}\">#{number}</a>#{line}"
line = p
if payload.fold && !payload.foldContinuation
div = document.createElement('div')
div.appendChild line
div.className = "fold #{payload.fold} show-first-line"
line = div
if payload.replace
if link = fragment.querySelector("#L#{number}")
link.parentElement.innerHTML = line.innerHTML
else
this.$("#L#{number}").parent().replaceWith line
else if payload.append
if link = fragment.querySelector("#L#{number}")
link.parentElement.innerHTML += line
else
this.$("#L#{number}").parent().append line
else if payload.foldContinuation
folds = fragment.querySelectorAll(".fold.#{payload.fold}")
if fold = folds[folds.length - 1]
fold.appendChild line
else
this.$("#log .fold.#{payload.fold}:last").append line
else
fragment.appendChild(line)
if payload.openFold
folds = fragment.querySelectorAll(".fold.#{payload.openFold}")
if fold = folds[folds.length - 1]
fold = $(fold)
else
fold = this.$(".fold.#{payload.openFold}:last")
fold.removeClass('show-first-line').addClass('open')
if payload.foldEnd
folds = fragment.querySelectorAll(".fold.#{payload.fold}")
if fold = folds[folds.length - 1]
fold = $(fold)
else
fold = this.$(".fold.#{payload.fold}:last")
fold.removeClass('show-first-line')
this.$('#log')[0].appendChild fragment
if cut
url = Travis.Urls.plainTextLog(@get('log.id'))
this.$("#log").append $("<p class=\"cut\">Log was too long to display. Download the <a href=\"#{url}\">the raw version</a> to get the full log.</p>")

View File

@ -0,0 +1,190 @@
require 'log'
Travis.reopen
LogView: Travis.View.extend
templateName: 'jobs/log'
logBinding: 'job.log'
contextBinding: 'job'
init: ->
@_super.apply this, arguments
plainTextLogUrl: (->
if id = @get('job.log.id')
Travis.Urls.plainTextLog(id)
).property('log')
didInsertElement: ->
job = @get('job')
job.subscribe() if job && !job.get('isFinished')
@_super.apply this, arguments
#@tryScrollingToHashLineNumber()
scrollTo: (hash) ->
# and this is even more weird, when changing hash in URL in firefox
# to other value, for example #L10, it actually scrolls just #main
# element... this is probably some CSS issue, I don't have time to
# investigate at the moment
# TODO: fix this
$('#main').scrollTop 0
# weird, html works in chrome, body in firefox
$('html,body').scrollTop $(hash).offset().top
@set 'controller.lineNumberHash', null
lineNumberHashDidChange: (->
@tryScrollingToHashLineNumber()
).observes('controller.lineNumberHash')
tryScrollingToHashLineNumber: ->
if hash = @get 'controller.lineNumberHash'
self = this
checker = ->
return if self.get('isDestroyed')
if $(hash).length
self.scrollTo(hash)
else
setTimeout checker, 100
checker()
click: ->
target = $(event.target)
target.closest('.fold').toggleClass('open')
if target.is('a') && target.attr('id') && target.attr('id').match(/^L\d+$/)
path = target.attr 'href'
Travis.get('router').route(path)
event.stopPropagation()
return false
toTop: () ->
$(window).scrollTop(0)
logUrl: (->
repo = @get('job.repo')
item = @get('parentView.currentItem')
if repo && item
event = if item.constructor == Travis.Build
'showBuild'
else
'showJob'
#Travis.get('router').urlForEvent(event, repo, item)
).property('job.repo', 'parentView.currentItem')
PreView: Em.View.extend
templateName: 'jobs/pre'
createEngine: ->
limit = new Log.Limit
scroll = new Log.Scroll
engine = Log.create(listeners: [limit, new Log.FragmentRenderer, new Log.Folds, scroll])
@set('scroll', scroll)
@set('limit', limit)
@set('engine', engine)
@observeParts()
observeParts: ->
parts = @get('log.parts')
parts.addArrayObserver(@, didChange: 'partsDidChange', willChange: 'partsWillChange')
@partsDidChange(parts.slice(0))
didInsertElement: ->
@_super.apply this, arguments
@createEngine()
willDestroyElement: ->
parts = @get('log.parts')
parts.removeArrayObserver(@, didChange: 'partsDidChange', willChange: 'partsWillChange')
partsWillChange: ->
partsDidChange: (parts, start, _, added) ->
unless @get('isLimited')
start ||= 0
added ||= parts.length
@get('engine').set(part.number, part.content) for part, i in parts.slice(start, start + added)
#@propertyDidChange('isLimited')
versionDidChange: (->
@rerender() if @get('inDOM')
).observes('log.version')
logDidChange: (->
@rerender() if @get('inDOM')
).observes('log')
isLimited: (->
@limit && @limit.isLimited()
).property()
plainTextLogUrl: (->
Travis.Urls.plainTextLog(id) if id = @get('log.job.id')
).property('job.log.id')
toggleTailing: (event) ->
Travis.app.tailing.toggle()
event.preventDefault()
lineNumbers: ->
$('#log').on 'mouseenter', 'a', ->
$(this).attr('href', '#L' + ($(this.parentNode).prevAll('p').length + 1))
folds: ->
$('#log').on 'click', '.fold', ->
$(this).toggleClass('open')
click: (event) ->
target = $(event.target)
target.closest('.fold').toggleClass('open')
if target.is('a') && matches = target.attr('href')?.match(/#L(\d+)$/)
Travis.app.get('router.location').setURL(target.attr('href'))
@set('controller.lineNumber', matches[1])
event.stopPropagation()
return false
lineNumberObserver: (->
@scroll.set(number) if !@get('isDestroyed') && number = @get('controller.lineNumber')
).observes('controller.lineNumber')
Log.Scroll = ->
Log.Scroll.prototype = $.extend new Log.Listener,
set: (number) ->
return unless number
@number = number
@tryScroll()
insert: (log, after, data) ->
@tryScroll() if @number
tryScroll: ->
if element = $("#log p:nth-child(#{@number})")
$('#main').scrollTop(0)
$('html, body').scrollTop(element.offset()?.top) # weird, html works in chrome, body in firefox
@highlight(element)
@number = undefined
highlight: (element) ->
$('#log p.highlight').removeClass('highlight')
$(element).addClass('highlight')
Log.Limit = ->
Log.Limit.prototype = $.extend new Log.Listener,
MAX_LINES: 5000
count: 0
insert: (log, after, lines) ->
@count += lines.length
lines.length = @MAX_LINES if lines.length > @MAX_LINES
isLimited: ->
@count > @MAX_LINES

View File

@ -1,152 +0,0 @@
# TODO: revisit those patterns
FOLDS = [
Em.Object.create(name: 'schema', startPattern: /^\$ (?:bundle exec )?rake( db:create)? db:schema:load/, endPattern: /^(<\/span>)?\$/)
Em.Object.create(name: 'migrate', startPattern: /^\$ (?:bundle exec )?rake( db:create)? db:migrate/, endPattern: /^(<\/span>)?\$/)
Em.Object.create(name: 'bundle', startPattern: /^\$ bundle install/, endPattern: /^(<\/span>)?\$/)
]
@Travis.Log = Em.Object.extend
init: ->
@set 'folds', []
@set 'line', 1
@set 'lineNumber', 1
@initial = true
for fold in FOLDS
@addFold fold
append: (lines) ->
return unless lines
return if @get('lineNumber') > 5000
log = @join lines
log = @escape log
log = @deansi log
lines = @split log
target = @get 'target'
index = 0
currentFold = @currentFold
result = []
for line in lines
if line == '\r'
@set 'replace', true
else if line == '\n'
@set 'newline', true
index += 1
else
if currentFold && ( @isFoldEnding(currentFold, line) )
# end of the fold, send fold to target
if result.length > 0
result.slice(-1)[0].foldEnd = true
target.appendLog result
@currentFold = currentFold = null
@set 'foldContinuation', false
result = []
if !currentFold && ( currentFold = @foldByStart(line) )
# beginning new fold, send current lines to target
if result.length > 0
target.appendLog result
result = []
start = index
payload = { content: line }
if currentFold
payload.fold = currentFold.get('name')
if @get 'foldContinuation'
payload.foldContinuation = true
payload.number = @get('lineNumber') + index
if @get 'replace'
@set 'replace', false
payload.replace = true
else if @get 'newline'
@set 'newline', false
else if !@initial
payload.append = true
@initial = false
if payload.foldContinuation && payload.content.match(/Done. Build script exited|Your build has been stopped/)
# script ended, but fold is still closed, which most probably means
# error, end the fold and open it.
# TODO: we need log marks to make it easier
payload.foldContinuation = null
payload.openFold = payload.fold
payload.fold = null
result.pushObject payload
if currentFold
@set 'foldContinuation', true
if @get('lineNumber') + index >= 5000
result.pushObject logWasCut: true
break
if result.length > 0
if currentFold
@currentFold = currentFold
target.appendLog result
nextLineNumber = @get('lineNumber') + index
@set 'lineNumber', nextLineNumber
join: (lines) ->
if typeof lines == 'string'
lines
else
lines.toArray().join ''
split: (log) ->
log = log.replace /\r\n/g, '\n'
lines = log.split(/(\n)/)
if lines.slice(-1)[0] == ''
lines.popObject()
result = []
for line in lines
result.pushObjects line.split(/(\r)/)
result
escape: (log) ->
Handlebars.Utils.escapeExpression log
deansi: (log) ->
log = log.replace(/\r\r/g, '\r')
.replace(/\033\[K\r/g, '\r')
.replace(/\[2K/g, '')
.replace(/\033\(B/g, '')
.replace(/\033\[\d+G/g, '')
ansi = ansiparse(log)
text = ''
ansi.forEach (part) ->
classes = []
part.foreground and classes.push(part.foreground)
part.background and classes.push('bg-' + part.background)
part.bold and classes.push('bold')
part.italic and classes.push('italic')
text += (if classes.length then ('<span class=\'' + classes.join(' ') + '\'>' + part.text + '</span>') else part.text)
text.replace /\033/g, ''
addFold: (fold) ->
@get('folds').pushObject fold
foldByStart: (line) ->
@get('folds').find (fold) -> line.match(fold.get('startPattern'))
isFoldEnding: (fold, line) ->
line.match(fold.get('endPattern'))