bosh_cli 1.2200.0 → 1.2291.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,9 @@ end
4
4
 
5
5
  require 'cli/task_tracking/task_tracker'
6
6
  require 'cli/task_tracking/total_duration'
7
+ require 'cli/task_tracking/smart_whitespace_printer'
7
8
  require 'cli/task_tracking/task_log_renderer'
8
9
  require 'cli/task_tracking/null_task_log_renderer'
9
- require 'cli/task_tracking/stage_progress_bar'
10
10
  require 'cli/task_tracking/stage_collection'
11
+ require 'cli/task_tracking/stage_collection_presenter'
11
12
  require 'cli/task_tracking/event_log_renderer'
@@ -1,3 +1,6 @@
1
+ require 'forwardable'
2
+ require 'json'
3
+
1
4
  module Bosh::Cli::TaskTracking
2
5
  class EventLogRenderer < TaskLogRenderer
3
6
  class InvalidEvent < StandardError; end
@@ -5,382 +8,101 @@ module Bosh::Cli::TaskTracking
5
8
  extend Forwardable
6
9
  def_delegators :@total_duration, :duration, :duration_known?, :started_at, :finished_at
7
10
 
8
- attr_reader :current_stage
9
- attr_reader :events_count
11
+ def initialize
12
+ super
10
13
 
11
- def initialize(options={})
14
+ @printer = SmartWhitespacePrinter.new
12
15
  @total_duration = TotalDuration.new
13
- @lock = Monitor.new
14
- @events_count = 0
15
- @seen_stages = Set.new
16
- @out = Bosh::Cli::Config.output || $stdout
17
- @out.sync = true
18
- @buffer = StringIO.new
19
- @progress_bars = {}
20
- @pos = 0
21
- @time_adjustment = 0
22
- @stages_without_progress_bar = options[:stages_without_progress_bar] || []
23
- end
24
-
25
- def add_output(output)
26
- output.to_s.split("\n").each { |line| add_event(line) }
27
- end
28
-
29
- def add_event(event_line)
30
- event = parse_event(event_line)
31
-
32
- @lock.synchronize do
33
- if event['type'] == 'deprecation'
34
- @buffer.puts("Deprecation: #{event['message']}".make_red)
35
- return
36
- end
37
-
38
- # Handling the special "error" event
39
- if event['error']
40
- done_with_stage if @current_stage
41
- add_error(event)
42
- return
43
- end
44
16
 
45
- if can_handle_event_without_progress_bar?(event)
46
- if @current_stage
47
- done_with_stage
48
- @current_stage = nil
49
- @buffer.print "\n"
50
- end
51
- handle_event_without_progress_bar(event)
52
- return
53
- end
17
+ presenter = StageCollectionPresenter.new(@printer)
54
18
 
55
- # One way to handle old stages is to prevent them
56
- # from appearing on screen altogether. That means
57
- # that we can always render the current stage only
58
- # and that simplifies housekeeping around progress
59
- # bars and messages. However we could always support
60
- # resuming the older stages rendering if we feel
61
- # that it's valuable.
62
-
63
- tags = event['tags'].is_a?(Array) ? event['tags'] : []
64
- stage_header = event['stage']
65
-
66
- if tags.size > 0
67
- stage_header += ' ' + tags.sort.join(', ').make_green
68
- end
19
+ @stage_collection = StageCollection.new(
20
+ stage_started: presenter.method(:start_stage),
21
+ stage_finished: presenter.method(:finish_stage),
22
+ stage_failed: presenter.method(:fail_stage),
69
23
 
70
- unless @seen_stages.include?(stage_header)
71
- done_with_stage if @current_stage
72
- begin_stage(event, stage_header)
73
- end
74
-
75
- if @current_stage == stage_header
76
- append_event(event)
77
- end
78
- end
79
-
80
- rescue InvalidEvent => e
81
- # Swallow for the moment
82
- end
83
-
84
- def begin_stage(event, header)
85
- @current_stage = header
86
- @seen_stages << @current_stage
87
-
88
- @stage_start_time = Time.at(event['time']) rescue Time.now
89
- @local_start_time = adjusted_time(@stage_start_time)
90
-
91
- @tasks = {}
92
- @done_tasks = []
93
-
94
- @eta = nil
95
- @stage_has_error = false # Error flag
96
- # Tracks max_in_flight best guess
97
- @tasks_batch_size = 0
98
- @batches_count = 0
99
-
100
- # Running average of task completion time
101
- @running_avg = 0
102
-
103
- append_stage_header
104
- end
105
-
106
- def render
107
- @lock.synchronize do
108
- @buffer.seek(@pos)
109
- output = @buffer.read
110
- @out.print output
111
- @pos = @buffer.tell
112
- output
113
- end
24
+ task_started: presenter.method(:start_task),
25
+ task_finished: presenter.method(:finish_task),
26
+ task_failed: presenter.method(:fail_task),
27
+ )
114
28
  end
115
29
 
116
- def add_error(event)
117
- error = event['error'] || {}
118
- code = error['code']
119
- message = error['message']
120
-
121
- error = 'Error'
122
- error += " #{code}" if code
123
- error += ": #{message}" if message
124
-
125
- @buffer.puts("\n" + error.make_red)
126
- end
30
+ alias_method :add_raw_output, :add_output
127
31
 
128
- def refresh
129
- # This is primarily used to refresh timer
130
- # without advancing rendering buffer
131
- @lock.synchronize do
132
- if @in_progress
133
- progress_bar.label = time_with_eta(Time.now - @local_start_time, @eta)
134
- progress_bar.refresh
32
+ def add_output(output)
33
+ output.to_s.split("\n").each do |line|
34
+ begin
35
+ event = parse_event(line)
36
+ add_event(event) if event
37
+ rescue InvalidEvent => e
38
+ @printer.print(:line_around, "Received invalid event: #{e.message}".make_red)
135
39
  end
136
- render
137
40
  end
41
+
42
+ add_raw_output(@printer.output)
138
43
  end
139
44
 
140
45
  def finish(state)
141
- return if @events_count == 0
142
-
143
- @lock.synchronize do
144
- @done = true
145
- done_with_stage(state)
146
- render
147
- end
46
+ @printer.finish
47
+ add_raw_output(@printer.output)
48
+ super
148
49
  end
149
50
 
150
51
  private
151
52
 
152
- def append_stage_header
153
- @buffer.print "\n#{@current_stage}\n"
154
- end
155
-
156
- def done_with_stage(state = nil)
157
- return unless @in_progress
158
-
159
- if @last_event
160
- completion_time = Time.at(@last_event['time']) rescue Time.now
161
- else
162
- completion_time = Time.now
163
- end
164
-
165
- if state.nil?
166
- state = @stage_has_error ? 'error' : 'done'
167
- end
168
-
169
- case state.to_s
170
- when 'done'
171
- progress_bar.title = 'Done'.make_green
172
- progress_bar.finished_steps = progress_bar.total
173
- when 'error'
174
- progress_bar.title = 'Error'.make_red
175
- else
176
- progress_bar.title = 'Not done'.make_yellow
177
- end
178
-
179
- progress_bar.bar_visible = false
180
- progress_bar.label = format_time(completion_time - @stage_start_time)
181
- progress_bar.refresh
182
- @buffer.print "\n"
183
- @in_progress = false
184
- end
185
-
186
- def progress_bar
187
- @progress_bars[@current_stage] ||= StageProgressBar.new(@buffer)
188
- end
189
-
190
- # We have to trust the first event in each stage
191
- # to have correct "total" and "current" fields.
192
- def append_event(event)
193
- validate_event(event)
194
-
195
- progress = 0
196
- total = event['total'].to_i
197
-
198
- if event['state'] == 'started'
199
- task = Task.new(event['task'])
200
- else
201
- task = @tasks[event['index']]
202
- end
203
-
204
- event_data = event['data'] || {}
205
- # Ignoring out-of-order events
206
- return if task.nil?
207
-
208
- @events_count += 1
209
- @last_event = event
210
-
211
- case event['state']
212
- when 'started'
213
- @total_duration.started_at = event['time']
214
-
215
- begin
216
- task.start_time = Time.at(event['time'])
217
- rescue
218
- task.start_time = Time.now
219
- end
220
-
221
- task.progress = 0
222
-
223
- @tasks[event['index']] = task
224
-
225
- if @tasks.size > @tasks_batch_size
226
- # Heuristics here: we assume that local maximum of
227
- # tasks number is a "max_in_flight" value and batches count
228
- # should only be recalculated once we refresh this maximum.
229
- # It's unlikely that the first task in a batch will be finished
230
- # before the last one is started so @done_tasks is expected
231
- # to only have canaries.
232
- @tasks_batch_size = @tasks.size
233
- @non_canary_event_start_time = task.start_time
234
- @batches_count = ((total - @done_tasks.size) / @tasks_batch_size.to_f).ceil
235
- end
236
-
237
- when 'finished', 'failed'
238
- @tasks.delete(event['index'])
239
- @done_tasks << task
240
-
241
- @total_duration.finished_at = event['time']
242
-
243
- begin
244
- task.finish_time = Time.at(event['time'])
245
- rescue
246
- task.finish_time = Time.now
247
- end
248
-
249
- task_time = task.finish_time - task.start_time
250
-
251
- n_done_tasks = @done_tasks.size.to_f
252
- @running_avg = @running_avg * (n_done_tasks - 1) / n_done_tasks + task_time.to_f / n_done_tasks
253
-
254
- progress = 1
255
- progress_bar.finished_steps += 1
256
- progress_bar.label = time_with_eta(task_time, @eta)
257
- progress_bar.clear_line
258
-
259
- task_name = task.name.to_s
260
- if task_name !~ /^[A-Z]{2}/
261
- task_name = task_name[0..0].to_s.downcase + task_name[1..-1].to_s
262
- end
53
+ def parse_event(event_line)
54
+ return if event_line.start_with?('#')
263
55
 
264
- if event['state'] == 'failed'
265
- status = [task_name.make_red, event_data['error']].compact.join(': ')
266
- @stage_has_error = true
267
- else
268
- status = task_name.make_yellow
56
+ JSON.parse(event_line).tap do |result|
57
+ unless result.kind_of?(Hash)
58
+ raise InvalidEvent, "Hash expected, #{result.class} given"
269
59
  end
270
- @buffer.puts(" #{status} (#{format_time(task_time)})")
271
-
272
- when 'in_progress'
273
- progress = [event['progress'].to_f / 100, 1].min
274
60
  end
275
61
 
276
- if @batches_count > 0 && @non_canary_event_start_time
277
- @eta = adjusted_time(@non_canary_event_start_time + @running_avg * @batches_count)
278
- end
279
-
280
- progress_bar_gain = progress - task.progress
281
- task.progress = progress
282
-
283
- progress_bar.total = total
284
- progress_bar.title = @tasks.values.map { |t| t.name }.sort.join(', ')
285
-
286
- progress_bar.current += progress_bar_gain
287
- progress_bar.refresh
288
-
289
- @in_progress = true
62
+ rescue JSON::JSONError => e
63
+ raise InvalidEvent, "Invalid JSON: #{e.message}"
290
64
  end
291
65
 
292
- def parse_event(event_line)
293
- event = JSON.parse(event_line)
294
- unless event.kind_of?(Hash)
295
- raise InvalidEvent, "Hash expected, #{event.class} given"
296
- end
297
- event
298
- rescue JSON::JSONError
299
- raise InvalidEvent, 'Cannot parse event, invalid JSON'
300
- end
66
+ def add_event(event)
67
+ @total_duration.started_at = event['time']
68
+ @total_duration.finished_at = event['time']
301
69
 
302
- def validate_event(event)
303
- unless event['time'] && event['stage'] && event['task'] &&
304
- event['index'] && event['total'] && event['state']
305
- raise InvalidEvent, 'Invalid event structure: stage, time, task, ' +
306
- 'index, total, state are all required'
70
+ if event['type'] == 'deprecation'
71
+ show_deprecation(event)
72
+ elsif event['error']
73
+ show_error(event)
74
+ else
75
+ show_stage_or_task(event)
307
76
  end
308
77
  end
309
78
 
310
- def time_with_eta(time, eta)
311
- time_fmt = format_time(time)
312
- eta_fmt = eta && eta > Time.now ? format_time(eta - Time.now) : '--:--:--'
313
- "#{time_fmt} ETA: #{eta_fmt}"
314
- end
315
-
316
- def adjusted_time(time)
317
- time + @time_adjustment.to_f
79
+ def show_deprecation(event)
80
+ msg = "Deprecation: #{event['message']}"
81
+ @printer.print(:line_around, msg.make_red)
318
82
  end
319
83
 
320
- def can_handle_event_without_progress_bar?(event)
321
- @stages_without_progress_bar.include?(event['stage'])
322
- end
323
-
324
- def handle_event_without_progress_bar(event)
325
- @total_duration.started_at = event['time']
326
- @total_duration.finished_at = event['time']
327
- stage_collection.update_with_event(event)
328
- end
329
-
330
- def stage_collection
331
- @stage_collection ||= StageCollection.new(
332
- stage_started: ->(stage){
333
- @buffer.print(" Started #{header_for_stage(stage)}\n")
334
- },
335
- stage_finished: ->(stage){
336
- duration = stage.duration ? " (#{format_time(stage.duration)})" : ''
337
- @buffer.print(" Done #{header_for_stage(stage)}#{duration}\n\n")
338
- },
339
- stage_failed: ->(stage){
340
- duration = stage.duration ? " (#{format_time(stage.duration)})" : ''
341
- @buffer.print(" Failed #{header_for_stage(stage)}#{duration}\n")
342
- },
343
-
344
- task_started: ->(task){
345
- @buffer.print(" Started #{header_for_task(task)}\n")
346
- },
347
- task_finished: ->(task){
348
- duration = task.duration ? " (#{format_time(task.duration)})" : ''
349
- @buffer.print(" Done #{header_for_task(task)}#{duration}\n")
350
- },
351
- task_failed: ->(task){
352
- error_msg = task.error
353
- error_msg = ": #{error_msg.make_red}" if error_msg
354
- duration = task.duration ? " (#{format_time(task.duration)})" : ''
355
- @buffer.print(" Failed #{header_for_task(task)}#{duration}#{error_msg}\n")
356
- },
357
- )
358
- end
84
+ def show_error(event)
85
+ error = event['error'] || {}
86
+ code = error['code']
87
+ message = error['message']
359
88
 
360
- def header_for_stage(stage)
361
- tags = stage.tags
362
- tags_str = tags.size > 0 ? ' ' + tags.sort.join(', ').make_green : ''
363
- "#{stage.name.downcase}#{tags_str}"
364
- end
89
+ msg = 'Error'
90
+ msg += " #{code}" if code
91
+ msg += ": #{message}" if message
365
92
 
366
- def header_for_task(task)
367
- tags = task.stage.tags
368
- tags_str = tags.size > 0 ? ' ' + tags.sort.join(', ').make_green : ''
369
- "#{task.stage.name.downcase}#{tags_str}: #{task.name}"
93
+ @printer.print(:line_around, msg.make_red)
370
94
  end
371
95
 
372
- class Task
373
- attr_accessor :name
374
- attr_accessor :progress
375
- attr_accessor :start_time
376
- attr_accessor :finish_time
96
+ REQUIRED_STAGE_EVENT_KEYS = %w(time stage task index total state).freeze
377
97
 
378
- def initialize(name)
379
- @name = name
380
- @progress = 0
381
- @start_time = nil
382
- @finish_time = nil
98
+ def show_stage_or_task(event)
99
+ REQUIRED_STAGE_EVENT_KEYS.each do |key|
100
+ unless event.has_key?(key)
101
+ raise InvalidEvent, "Missing event key: #{key}"
102
+ end
383
103
  end
104
+
105
+ @stage_collection.update_with_event(event)
384
106
  end
385
107
  end
386
108
  end
@@ -0,0 +1,56 @@
1
+ require 'stringio'
2
+
3
+ module Bosh::Cli::TaskTracking
4
+ class SmartWhitespacePrinter
5
+ VALID_SEPARATORS = [:line_around, :line_before, :before, :none].freeze
6
+
7
+ SPACE_BETWEEN_LAST_SEP_AND_SEP = {
8
+ [:line_around, :line_around] => "\n\n",
9
+ [:line_around, :line_before] => "\n\n",
10
+ [:line_around, :before] => "\n\n",
11
+ [:line_around, :none] => "\n\n",
12
+
13
+ [:line_before, :line_around] => "\n\n",
14
+ [:line_before, :line_before] => "\n\n",
15
+ [:line_before, :before] => "\n",
16
+ [:line_before, :none] => nil,
17
+
18
+ [:before, :line_around] => "\n\n",
19
+ [:before, :line_before] => "\n\n",
20
+ [:before, :before] => "\n",
21
+ [:before, :none] => nil,
22
+
23
+ [:none, :line_around] => "\n\n",
24
+ [:none, :line_before] => "\n\n",
25
+ [:none, :before] => "\n",
26
+ [:none, :none] => nil,
27
+ }.freeze
28
+
29
+ def initialize
30
+ @buffer = StringIO.new
31
+ @last_sep = :start
32
+ end
33
+
34
+ def print(separator, msg)
35
+ unless VALID_SEPARATORS.include?(separator)
36
+ raise ArgumentError, "Unknown separator #{separator.inspect}"
37
+ end
38
+
39
+ space = SPACE_BETWEEN_LAST_SEP_AND_SEP[[@last_sep, separator]]
40
+ @buffer.print(space) if space
41
+
42
+ @last_sep = separator
43
+ @buffer.print(msg)
44
+ end
45
+
46
+ def output
47
+ @buffer.string.tap { @buffer.string = '' }
48
+ end
49
+
50
+ def finish
51
+ if VALID_SEPARATORS.include?(@last_sep)
52
+ @buffer.print("\n")
53
+ end
54
+ end
55
+ end
56
+ end
@@ -9,7 +9,7 @@ module Bosh::Cli::TaskTracking
9
9
 
10
10
  def update_with_event(event)
11
11
  new_stage = Stage.new(event['stage'], event['tags'], event['total'], @callbacks)
12
- unless found_stage = @stages.find { |s| s.name == new_stage.name && s.tags == new_stage.tags }
12
+ unless found_stage = @stages.find { |s| s == new_stage }
13
13
  found_stage = new_stage
14
14
  @stages << new_stage
15
15
  end
@@ -31,14 +31,14 @@ module Bosh::Cli::TaskTracking
31
31
 
32
32
  def update_with_event(event)
33
33
  new_task = Task.new(self, event['task'], event['progress'], @callbacks)
34
- unless found_task = @tasks.find { |t| t.name == new_task.name }
34
+ unless found_task = @tasks.find { |s| s == new_task }
35
35
  found_task = new_task
36
36
  @tasks << new_task
37
37
  end
38
- fire_started_callback(event)
38
+ fire_started_callback if started?
39
39
  found_task.update_with_event(event)
40
- fire_finished_callback(event)
41
- fire_failed_callback(event)
40
+ fire_finished_callback if finished?(event)
41
+ fire_failed_callback if failed?(event)
42
42
  found_task
43
43
  end
44
44
 
@@ -55,28 +55,59 @@ module Bosh::Cli::TaskTracking
55
55
  total_duration.duration
56
56
  end
57
57
 
58
+ def similar?(other)
59
+ return false unless other.is_a?(Stage)
60
+ name == other.name
61
+ end
62
+
63
+ def ==(other)
64
+ return false unless other.is_a?(Stage)
65
+ [name, tags, total] == [other.name, other.tags, other.total]
66
+ end
67
+
58
68
  private
59
69
 
60
- def fire_started_callback(event)
61
- if event['state'] == 'started' && event['index'] == 1
62
- callback = @callbacks[:stage_started]
63
- callback.call(self) if callback
64
- end
70
+ def fire_started_callback
71
+ callback = @callbacks[:stage_started]
72
+ callback.call(self) if callback
65
73
  end
66
74
 
67
- def fire_finished_callback(event)
68
- if event['state'] == 'finished' && ((event['index'] == event['total']) || event['total'].nil?)
69
- callback = @callbacks[:stage_finished]
70
- callback.call(self) if callback
71
- end
75
+ def fire_finished_callback
76
+ callback = @callbacks[:stage_finished]
77
+ callback.call(self) if callback
72
78
  end
73
79
 
74
- def fire_failed_callback(event)
75
- if event['state'] == 'failed'
76
- # If there are multiple failures do we need to only fire on the first one?
77
- callback = @callbacks[:stage_failed]
78
- callback.call(self) if callback
79
- end
80
+ def fire_failed_callback
81
+ callback = @callbacks[:stage_failed]
82
+ callback.call(self) if callback
83
+ end
84
+
85
+ def started?
86
+ @started = true if !@started
87
+ end
88
+
89
+ def finished?(event)
90
+ seen_all_tasks?(event) && all_tasks_finished?
91
+ end
92
+
93
+ def failed?(event)
94
+ seen_all_tasks?(event) && all_tasks_done? && any_tasks_failed?
95
+ end
96
+
97
+ def all_tasks_done?
98
+ tasks.all? { |t| t.done? }
99
+ end
100
+
101
+ def all_tasks_finished?
102
+ tasks.all? { |t| t.finished? }
103
+ end
104
+
105
+ def any_tasks_failed?
106
+ tasks.any? { |t| t.failed? }
107
+ end
108
+
109
+ def seen_all_tasks?(event)
110
+ tasks.size == event['total'] || event['total'].nil?
80
111
  end
81
112
  end
82
113
 
@@ -105,6 +136,23 @@ module Bosh::Cli::TaskTracking
105
136
  call_state_callback
106
137
  end
107
138
 
139
+ def ==(other)
140
+ return false unless other.is_a?(Task)
141
+ [stage, name] == [other.stage, other.name]
142
+ end
143
+
144
+ def done?
145
+ %w(failed finished).include?(@state)
146
+ end
147
+
148
+ def failed?
149
+ @state == 'failed'
150
+ end
151
+
152
+ def finished?
153
+ @state == 'finished'
154
+ end
155
+
108
156
  private
109
157
 
110
158
  def call_state_callback
@@ -0,0 +1,111 @@
1
+ module Bosh::Cli::TaskTracking
2
+ class StageCollectionPresenter
3
+ JUSTIFY = 9
4
+
5
+ def initialize(printer)
6
+ @printer = printer
7
+ @last_stage = nil
8
+ @last_task = nil
9
+ end
10
+
11
+ def start_stage(stage)
12
+ msg_new_line = " Started #{header_for_stage(stage)}"
13
+
14
+ # Assume that duplicate start events are never received
15
+ if stage.similar?(@last_stage)
16
+ @printer.print(:before, msg_new_line)
17
+ else
18
+ @printer.print(:line_before, msg_new_line)
19
+ end
20
+
21
+ @last_stage = stage
22
+ @last_task = nil
23
+ end
24
+
25
+ def finish_stage(stage)
26
+ end_stage(stage, 'Done')
27
+ end
28
+
29
+ def fail_stage(stage)
30
+ end_stage(stage, 'Failed')
31
+ end
32
+
33
+ def end_stage(stage, prefix_msg)
34
+ duration = duration_str(stage)
35
+ msg = "#{prefix_msg.rjust(JUSTIFY)} #{header_for_stage(stage)}#{duration}"
36
+
37
+ if stage == @last_stage
38
+ if stage.total == 1
39
+ # end_task added inline end message
40
+ else
41
+ @printer.print(:before, msg)
42
+ end
43
+ else
44
+ @printer.print(:line_before, msg)
45
+ end
46
+
47
+ @last_stage = stage
48
+ @last_task = nil
49
+ end
50
+
51
+ def start_task(task)
52
+ msg = " Started #{full_header_for_task(task)}"
53
+
54
+ if task.stage == @last_stage && task.stage.total == 1
55
+ @printer.print(:none, " > #{task.name.make_green}")
56
+ elsif task.stage.similar?(@last_stage)
57
+ @printer.print(:before, msg)
58
+ else
59
+ @printer.print(:line_before, msg)
60
+ end
61
+
62
+ @last_stage = task.stage
63
+ @last_task = task
64
+ end
65
+
66
+ def finish_task(task)
67
+ end_task(task, 'Done', nil)
68
+ end
69
+
70
+ def fail_task(task)
71
+ error_msg = task.error
72
+ error_msg = ": #{error_msg.make_red}" if error_msg
73
+
74
+ end_task(task, 'Failed', error_msg)
75
+ end
76
+
77
+ def end_task(task, prefix_msg, suffix_msg)
78
+ duration = duration_str(task)
79
+ msg = "#{prefix_msg.rjust(JUSTIFY)} #{full_header_for_task(task)}#{suffix_msg}#{duration}"
80
+
81
+ if task == @last_task
82
+ @printer.print(:none, ". #{prefix_msg}#{suffix_msg}#{duration}")
83
+ elsif task.stage.similar?(@last_stage)
84
+ @printer.print(:before, msg)
85
+ else
86
+ @printer.print(:line_before, msg)
87
+ end
88
+
89
+ @last_stage = task.stage
90
+ @last_task = task
91
+ end
92
+
93
+ private
94
+
95
+ def header_for_stage(stage)
96
+ "#{stage.name.downcase}#{tags_for_stage(stage)}"
97
+ end
98
+
99
+ def full_header_for_task(task)
100
+ "#{task.stage.name.downcase}#{tags_for_stage(task.stage)} > #{task.name.make_green}"
101
+ end
102
+
103
+ def tags_for_stage(stage)
104
+ stage.tags.size > 0 ? ' ' + stage.tags.sort.join(', ').make_green : ''
105
+ end
106
+
107
+ def duration_str(stage_or_task)
108
+ stage_or_task.duration ? " (#{format_time(stage_or_task.duration)})" : ''
109
+ end
110
+ end
111
+ end
@@ -1,20 +1,8 @@
1
1
  module Bosh::Cli::TaskTracking
2
2
  class TaskLogRenderer
3
- EVENT_LOG_STAGES_WITHOUT_PROGRESS_BAR = [
4
- 'Preparing DNS',
5
- 'Creating bound missing VMs',
6
- 'Binding instance VMs',
7
- 'Updating job',
8
- 'Deleting unneeded instances',
9
- 'Running errand',
10
- 'Deleting instances',
11
- 'Refilling resource pools',
12
- ]
13
-
14
3
  def self.create_for_log_type(log_type)
15
4
  if log_type == 'event'
16
- EventLogRenderer.new(stages_without_progress_bar:
17
- EVENT_LOG_STAGES_WITHOUT_PROGRESS_BAR)
5
+ EventLogRenderer.new
18
6
  elsif log_type == 'result' || log_type == 'none'
19
7
  NullTaskLogRenderer.new
20
8
  else
@@ -28,16 +16,12 @@ module Bosh::Cli::TaskTracking
28
16
  def initialize
29
17
  @out = Bosh::Cli::Config.output || $stdout
30
18
  @out.sync = true
31
- @lock = Mutex.new
32
19
  @output = ''
20
+
33
21
  @time_adjustment = 0
34
22
  @duration = nil
35
23
  end
36
24
 
37
- def duration_known?
38
- false
39
- end
40
-
41
25
  def add_output(output)
42
26
  @output = output
43
27
  end
@@ -49,7 +33,10 @@ module Bosh::Cli::TaskTracking
49
33
 
50
34
  def finish(state)
51
35
  refresh
52
- @done = true
36
+ end
37
+
38
+ def duration_known?
39
+ false
53
40
  end
54
41
  end
55
42
  end
@@ -1,5 +1,5 @@
1
1
  module Bosh
2
2
  module Cli
3
- VERSION = '1.2200.0'
3
+ VERSION = '1.2291.0'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bosh_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2200.0
4
+ version: 1.2291.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-03-14 00:00:00.000000000 Z
12
+ date: 2014-03-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bosh_common
@@ -18,7 +18,7 @@ dependencies:
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: 1.2200.0
21
+ version: 1.2291.0
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
- version: 1.2200.0
29
+ version: 1.2291.0
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: json_pure
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -114,7 +114,7 @@ dependencies:
114
114
  requirements:
115
115
  - - ~>
116
116
  - !ruby/object:Gem::Version
117
- version: 1.2200.0
117
+ version: 1.2291.0
118
118
  type: :runtime
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
@@ -122,7 +122,7 @@ dependencies:
122
122
  requirements:
123
123
  - - ~>
124
124
  - !ruby/object:Gem::Version
125
- version: 1.2200.0
125
+ version: 1.2291.0
126
126
  - !ruby/object:Gem::Dependency
127
127
  name: net-ssh
128
128
  requirement: !ruby/object:Gem::Requirement
@@ -269,7 +269,7 @@ dependencies:
269
269
  version: '0'
270
270
  description: ! 'BOSH CLI
271
271
 
272
- 87cbf9'
272
+ 51a1bb'
273
273
  email: support@cloudfoundry.com
274
274
  executables:
275
275
  - bosh
@@ -346,8 +346,9 @@ files:
346
346
  - lib/cli/task_tracking.rb
347
347
  - lib/cli/task_tracking/event_log_renderer.rb
348
348
  - lib/cli/task_tracking/null_task_log_renderer.rb
349
+ - lib/cli/task_tracking/smart_whitespace_printer.rb
349
350
  - lib/cli/task_tracking/stage_collection.rb
350
- - lib/cli/task_tracking/stage_progress_bar.rb
351
+ - lib/cli/task_tracking/stage_collection_presenter.rb
351
352
  - lib/cli/task_tracking/task_log_renderer.rb
352
353
  - lib/cli/task_tracking/task_tracker.rb
353
354
  - lib/cli/task_tracking/total_duration.rb
@@ -379,7 +380,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
379
380
  version: '0'
380
381
  segments:
381
382
  - 0
382
- hash: 1307915788410922984
383
+ hash: -3167564348924441903
383
384
  requirements: []
384
385
  rubyforge_project:
385
386
  rubygems_version: 1.8.23
@@ -1,59 +0,0 @@
1
- module Bosh::Cli::TaskTracking
2
- class StageProgressBar
3
- attr_accessor :total
4
- attr_accessor :title
5
- attr_accessor :current
6
- attr_accessor :label
7
- attr_accessor :bar_visible
8
- attr_accessor :finished_steps
9
- attr_accessor :terminal_width
10
-
11
- def initialize(output)
12
- @output = output
13
- @current = 0
14
- @total = 100
15
- @bar_visible = true
16
- @finished_steps = 0
17
- @filler = 'o'
18
- @terminal_width = calculate_terminal_width
19
- @bar_width = (0.24 * @terminal_width).to_i # characters
20
- end
21
-
22
- def refresh
23
- clear_line
24
- bar_repr = @bar_visible ? bar : ''
25
- title_width = (0.35 * @terminal_width).to_i
26
- title = @title.truncate(title_width).ljust(title_width)
27
- @output.print "#{title} #{bar_repr} #{@finished_steps}/#{@total}"
28
- @output.print " #{@label}" if @label
29
- end
30
-
31
- def bar
32
- n_fillers = @total == 0 ? 0 : [(@bar_width *
33
- (@current.to_f / @total.to_f)).floor, 0].max
34
-
35
- fillers = "#{@filler}" * n_fillers
36
- spaces = ' ' * [(@bar_width - n_fillers), 0].max
37
- "|#{fillers}#{spaces}|"
38
- end
39
-
40
- def clear_line
41
- @output.print("\r")
42
- @output.print(' ' * @terminal_width)
43
- @output.print("\r")
44
- end
45
-
46
- def calculate_terminal_width
47
- if ENV['COLUMNS'].to_s =~ /^\d+$/
48
- ENV['COLUMNS'].to_i
49
- elsif !ENV['TERM'].blank?
50
- width = `tput cols`
51
- $?.exitstatus == 0 ? [width.to_i, 100].min : 80
52
- else
53
- 80
54
- end
55
- rescue
56
- 80
57
- end
58
- end
59
- end