Skip to content
Snippets Groups Projects
Commit baef6728 authored by Kamil Trzcinski's avatar Kamil Trzcinski
Browse files

Send trace to a browser incrementally when build is running

We send a state of ansi2html to client, client needs to send this state back.
The state describes the configuration of generator and position within trace.
parent 44501820
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -38,6 +38,14 @@ class Projects::BuildsController < Projects::ApplicationController
end
end
 
def trace
respond_to do |format|
format.json do
render json: @build.trace_with_state(params_state).merge!(id: @build.id, status: @build.status)
end
end
end
def retry
unless @build.retryable?
return render_404
Loading
Loading
@@ -72,6 +80,13 @@ class Projects::BuildsController < Projects::ApplicationController
 
private
 
def params_state
begin
JSON.parse(params[:state], symbolize_names: true)
rescue
end
end
def build
@build ||= project.builds.unscoped.find_by!(id: params[:id])
end
Loading
Loading
Loading
Loading
@@ -132,8 +132,12 @@ module Ci
end
 
def trace_html
html = Ci::Ansi2html::convert(trace) if trace.present?
html || ''
trace_with_state[:html]
end
def trace_with_state(state = nil)
trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present?
trace_with_state || {}
end
 
def timeout
Loading
Loading
- page_title "#{@build.name} (##{@build.id})", "Builds"
= render "header_title"
- trace = build.trace_for_state
 
.build-page
.row-content-block.top-block
Loading
Loading
@@ -85,7 +86,9 @@
%pre.trace#build-trace
%code.bash
= preserve do
= raw @build.trace_html
= raw trace[:html]
- if @build.active?
%i{:class => "fa fa-refresh fa-spin"}
 
%div#down-build-trace
 
Loading
Loading
@@ -216,4 +219,4 @@
 
 
:javascript
new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}")
new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace[:state]}")
Loading
Loading
@@ -672,6 +672,7 @@ Rails.application.routes.draw do
post :cancel
post :retry
post :erase
get :trace
get :raw
end
 
Loading
Loading
Loading
Loading
@@ -23,8 +23,8 @@ module Ci
cross: 0x10,
}
 
def self.convert(ansi)
Converter.new().convert(ansi)
def self.convert(ansi, state = nil)
Converter.new.convert(ansi, state)
end
 
class Converter
Loading
Loading
@@ -84,22 +84,36 @@ module Ci
def on_107(s) set_bg_color(7, 'l') end
def on_109(s) set_bg_color(9, 'l') end
 
def convert(ansi)
@out = ""
@n_open_tags = 0
reset()
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask]
def convert(raw, new_state)
reset_state
restore_state(new_state) if new_state && new_state[:offset].to_i < raw.length
start = @offset
ansi = raw[@offset..-1]
open_new_tag
 
s = StringScanner.new(ansi.gsub("<", "&lt;"))
s = StringScanner.new(ansi)
while(!s.eos?)
if s.scan(/\e([@-_])(.*?)([@-~])/)
handle_sequence(s)
elsif s.scan(/\e(([@-_])(.*?)?)?$/)
break
elsif s.scan(/</)
@out << '&lt;'
else
@out << s.scan(/./m)
end
@offset += s.matched_size
end
 
close_open_tags()
@out
{ state: state, html: @out, text: ansi[0, @offset - start], append: start > 0 }
end
 
def handle_sequence(s)
Loading
Loading
@@ -121,6 +135,20 @@ module Ci
 
evaluate_command_stack(commands)
 
open_new_tag
end
def evaluate_command_stack(stack)
return unless command = stack.shift()
if self.respond_to?("on_#{command}", true)
self.send("on_#{command}", stack)
end
evaluate_command_stack(stack)
end
def open_new_tag
css_classes = []
 
unless @fg_color.nil?
Loading
Loading
@@ -138,20 +166,8 @@ module Ci
css_classes << "term-#{css_class}" if @style_mask & flag != 0
end
 
open_new_tag(css_classes) if css_classes.length > 0
end
def evaluate_command_stack(stack)
return unless command = stack.shift()
if self.respond_to?("on_#{command}", true)
self.send("on_#{command}", stack)
end
evaluate_command_stack(stack)
end
return if css_classes.empty?
 
def open_new_tag(css_classes)
@out << %{<span class="#{css_classes.join(' ')}">}
@n_open_tags += 1
end
Loading
Loading
@@ -163,6 +179,26 @@ module Ci
end
end
 
def reset_state
@offset = 0
@n_open_tags = 0
@out = ''
reset
end
def state
STATE_PARAMS.inject({}) do |h, param|
h[param] = send(param)
h
end
end
def restore_state(new_state)
STATE_PARAMS.each do |param|
send("#{param}=".to_sym, new_state[param])
end
end
def reset
@fg_color = nil
@bg_color = nil
Loading
Loading
Loading
Loading
@@ -4,131 +4,176 @@ describe Ci::Ansi2html, lib: true do
subject { Ci::Ansi2html }
 
it "prints non-ansi as-is" do
expect(subject.convert("Hello")).to eq('Hello')
expect(subject.convert("Hello")[:html]).to eq('Hello')
end
 
it "strips non-color-changing controll sequences" do
expect(subject.convert("Hello \e[2Kworld")).to eq('Hello world')
expect(subject.convert("Hello \e[2Kworld")[:html]).to eq('Hello world')
end
 
it "prints simply red" do
expect(subject.convert("\e[31mHello\e[0m")).to eq('<span class="term-fg-red">Hello</span>')
expect(subject.convert("\e[31mHello\e[0m")[:html]).to eq('<span class="term-fg-red">Hello</span>')
end
 
it "prints simply red without trailing reset" do
expect(subject.convert("\e[31mHello")).to eq('<span class="term-fg-red">Hello</span>')
expect(subject.convert("\e[31mHello")[:html]).to eq('<span class="term-fg-red">Hello</span>')
end
 
it "prints simply yellow" do
expect(subject.convert("\e[33mHello\e[0m")).to eq('<span class="term-fg-yellow">Hello</span>')
expect(subject.convert("\e[33mHello\e[0m")[:html]).to eq('<span class="term-fg-yellow">Hello</span>')
end
 
it "prints default on blue" do
expect(subject.convert("\e[39;44mHello")).to eq('<span class="term-bg-blue">Hello</span>')
expect(subject.convert("\e[39;44mHello")[:html]).to eq('<span class="term-bg-blue">Hello</span>')
end
 
it "prints red on blue" do
expect(subject.convert("\e[31;44mHello")).to eq('<span class="term-fg-red term-bg-blue">Hello</span>')
expect(subject.convert("\e[31;44mHello")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span>')
end
 
it "resets colors after red on blue" do
expect(subject.convert("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world')
expect(subject.convert("\e[31;44mHello\e[0m world")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world')
end
 
it "performs color change from red/blue to yellow/blue" do
expect(subject.convert("\e[31;44mHello \e[33mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>')
expect(subject.convert("\e[31;44mHello \e[33mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>')
end
 
it "performs color change from red/blue to yellow/green" do
expect(subject.convert("\e[31;44mHello \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>')
expect(subject.convert("\e[31;44mHello \e[33;42mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>')
end
 
it "performs color change from red/blue to reset to yellow/green" do
expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>')
expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>')
end
 
it "ignores unsupported codes" do
expect(subject.convert("\e[51mHello\e[0m")).to eq('Hello')
expect(subject.convert("\e[51mHello\e[0m")[:html]).to eq('Hello')
end
 
it "prints light red" do
expect(subject.convert("\e[91mHello\e[0m")).to eq('<span class="term-fg-l-red">Hello</span>')
expect(subject.convert("\e[91mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red">Hello</span>')
end
 
it "prints default on light red" do
expect(subject.convert("\e[101mHello\e[0m")).to eq('<span class="term-bg-l-red">Hello</span>')
expect(subject.convert("\e[101mHello\e[0m")[:html]).to eq('<span class="term-bg-l-red">Hello</span>')
end
 
it "performs color change from red/blue to default/blue" do
expect(subject.convert("\e[31;44mHello \e[39mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
expect(subject.convert("\e[31;44mHello \e[39mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
end
 
it "performs color change from light red/blue to default/blue" do
expect(subject.convert("\e[91;44mHello \e[39mworld")).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
expect(subject.convert("\e[91;44mHello \e[39mworld")[:html]).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
end
 
it "prints bold text" do
expect(subject.convert("\e[1mHello")).to eq('<span class="term-bold">Hello</span>')
expect(subject.convert("\e[1mHello")[:html]).to eq('<span class="term-bold">Hello</span>')
end
 
it "resets bold text" do
expect(subject.convert("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span> world')
expect(subject.convert("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span> world')
expect(subject.convert("\e[1mHello\e[21m world")[:html]).to eq('<span class="term-bold">Hello</span> world')
expect(subject.convert("\e[1mHello\e[22m world")[:html]).to eq('<span class="term-bold">Hello</span> world')
end
 
it "prints italic text" do
expect(subject.convert("\e[3mHello")).to eq('<span class="term-italic">Hello</span>')
expect(subject.convert("\e[3mHello")[:html]).to eq('<span class="term-italic">Hello</span>')
end
 
it "resets italic text" do
expect(subject.convert("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span> world')
expect(subject.convert("\e[3mHello\e[23m world")[:html]).to eq('<span class="term-italic">Hello</span> world')
end
 
it "prints underlined text" do
expect(subject.convert("\e[4mHello")).to eq('<span class="term-underline">Hello</span>')
expect(subject.convert("\e[4mHello")[:html]).to eq('<span class="term-underline">Hello</span>')
end
 
it "resets underlined text" do
expect(subject.convert("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span> world')
expect(subject.convert("\e[4mHello\e[24m world")[:html]).to eq('<span class="term-underline">Hello</span> world')
end
 
it "prints concealed text" do
expect(subject.convert("\e[8mHello")).to eq('<span class="term-conceal">Hello</span>')
expect(subject.convert("\e[8mHello")[:html]).to eq('<span class="term-conceal">Hello</span>')
end
 
it "resets concealed text" do
expect(subject.convert("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span> world')
expect(subject.convert("\e[8mHello\e[28m world")[:html]).to eq('<span class="term-conceal">Hello</span> world')
end
 
it "prints crossed-out text" do
expect(subject.convert("\e[9mHello")).to eq('<span class="term-cross">Hello</span>')
expect(subject.convert("\e[9mHello")[:html]).to eq('<span class="term-cross">Hello</span>')
end
 
it "resets crossed-out text" do
expect(subject.convert("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span> world')
expect(subject.convert("\e[9mHello\e[29m world")[:html]).to eq('<span class="term-cross">Hello</span> world')
end
 
it "can print 256 xterm fg colors" do
expect(subject.convert("\e[38;5;16mHello")).to eq('<span class="xterm-fg-16">Hello</span>')
expect(subject.convert("\e[38;5;16mHello")[:html]).to eq('<span class="xterm-fg-16">Hello</span>')
end
 
it "can print 256 xterm fg colors on normal magenta background" do
expect(subject.convert("\e[38;5;16;45mHello")).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>')
expect(subject.convert("\e[38;5;16;45mHello")[:html]).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>')
end
 
it "can print 256 xterm bg colors" do
expect(subject.convert("\e[48;5;240mHello")).to eq('<span class="xterm-bg-240">Hello</span>')
expect(subject.convert("\e[48;5;240mHello")[:html]).to eq('<span class="xterm-bg-240">Hello</span>')
end
 
it "can print 256 xterm bg colors on normal magenta foreground" do
expect(subject.convert("\e[48;5;16;35mHello")).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>')
expect(subject.convert("\e[48;5;16;35mHello")[:html]).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>')
end
 
it "prints bold colored text vividly" do
expect(subject.convert("\e[1;31mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
expect(subject.convert("\e[1;31mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
end
 
it "prints bold light colored text correctly" do
expect(subject.convert("\e[1;91mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
expect(subject.convert("\e[1;91mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
end
it "prints &lt;" do
expect(subject.convert("<")[:html]).to eq('&lt;')
end
describe "incremental update" do
shared_examples 'stateable converter' do
let(:pass1) { subject.convert(pre_text) }
let(:pass2) { subject.convert(pre_text + text, pass1[:state]) }
it "to returns html to append" do
expect(pass2[:append]).to be_truthy
expect(pass2[:html]).to eq(html)
expect(pass1[:text] + pass2[:text]).to eq(pre_text + text)
expect(pass1[:html] + pass2[:html]).to eq(pre_html + html)
end
end
context "with split word" do
let(:pre_text) { "\e[1mHello" }
let(:pre_html) { "<span class=\"term-bold\">Hello</span>" }
let(:text) { "\e[1mWorld" }
let(:html) { "<span class=\"term-bold\"></span><span class=\"term-bold\">World</span>" }
it_behaves_like 'stateable converter'
end
context "with split sequence" do
let(:pre_text) { "\e[1m" }
let(:pre_html) { "<span class=\"term-bold\"></span>" }
let(:text) { "Hello" }
let(:html) { "<span class=\"term-bold\">Hello</span>" }
it_behaves_like 'stateable converter'
end
context "with partial sequence" do
let(:pre_text) { "Hello\e" }
let(:pre_html) { "Hello" }
let(:text) { "[1m World" }
let(:html) { "<span class=\"term-bold\"> World</span>" }
it_behaves_like 'stateable converter'
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment