fasten 0.4.0 → 0.5.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -2
- data/fasten.gemspec +2 -0
- data/lib/fasten/dag.rb +1 -1
- data/lib/fasten/executor.rb +61 -19
- data/lib/fasten/load_save.rb +9 -5
- data/lib/fasten/{log_support.rb → logger.rb} +4 -4
- data/lib/fasten/stats.rb +55 -6
- data/lib/fasten/task.rb +1 -1
- data/lib/fasten/ui/console.rb +58 -0
- data/lib/fasten/ui/curses.rb +256 -0
- data/lib/fasten/ui.rb +7 -155
- data/lib/fasten/version.rb +1 -1
- data/lib/fasten/worker.rb +43 -13
- data/lib/fasten.rb +18 -2
- metadata +33 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: ba214b0518a9968fceffca5ba9ce8ed32e845ecbacb56f153e14cd63fded465b
         | 
| 4 | 
            +
              data.tar.gz: 6fb2041f89edda667b3b35522ac50503e90e93e7cbe6ecb5e2e93de46b45171d
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: e49e576f5ed462ec03884818a1fb24b39177bdc0661b5a028338c1160a1be301e71fdb7f109dfdcb306adae2c0153266be0245b650ecb8c1d2c23e5438f1708c
         | 
| 7 | 
            +
              data.tar.gz: 674d4a02af2c36b36a2581f609ec13ba71ca8430ab0dd10060f2d0995d0fe5221969b70a9becf606abd7901f88fbbee63816eefc7482866faccc2d323b612303
         | 
    
        data/Gemfile.lock
    CHANGED
    
    | @@ -1,9 +1,11 @@ | |
| 1 1 | 
             
            PATH
         | 
| 2 2 | 
             
              remote: .
         | 
| 3 3 | 
             
              specs:
         | 
| 4 | 
            -
                fasten (0. | 
| 4 | 
            +
                fasten (0.5.0)
         | 
| 5 5 | 
             
                  binding_of_caller
         | 
| 6 6 | 
             
                  curses
         | 
| 7 | 
            +
                  hirb
         | 
| 8 | 
            +
                  parallel
         | 
| 7 9 |  | 
| 8 10 | 
             
            GEM
         | 
| 9 11 | 
             
              remote: https://rubygems.org/
         | 
| @@ -12,9 +14,10 @@ GEM | |
| 12 14 | 
             
                binding_of_caller (0.8.0)
         | 
| 13 15 | 
             
                  debug_inspector (>= 0.0.1)
         | 
| 14 16 | 
             
                coderay (1.1.2)
         | 
| 15 | 
            -
                curses (1.2. | 
| 17 | 
            +
                curses (1.2.5)
         | 
| 16 18 | 
             
                debug_inspector (0.0.3)
         | 
| 17 19 | 
             
                diff-lcs (1.3)
         | 
| 20 | 
            +
                hirb (0.7.3)
         | 
| 18 21 | 
             
                jaro_winkler (1.5.1)
         | 
| 19 22 | 
             
                method_source (0.9.0)
         | 
| 20 23 | 
             
                parallel (1.12.1)
         | 
    
        data/fasten.gemspec
    CHANGED
    
    | @@ -30,6 +30,8 @@ Gem::Specification.new do |spec| | |
| 30 30 |  | 
| 31 31 | 
             
              spec.add_runtime_dependency 'binding_of_caller'
         | 
| 32 32 | 
             
              spec.add_runtime_dependency 'curses'
         | 
| 33 | 
            +
              spec.add_runtime_dependency 'hirb'
         | 
| 34 | 
            +
              spec.add_runtime_dependency 'parallel'
         | 
| 33 35 |  | 
| 34 36 | 
             
              raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' unless spec.respond_to?(:metadata)
         | 
| 35 37 |  | 
    
        data/lib/fasten/dag.rb
    CHANGED
    
    
    
        data/lib/fasten/executor.rb
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            module Fasten
         | 
| 2 2 | 
             
              class Executor < Task
         | 
| 3 | 
            -
                include Fasten:: | 
| 3 | 
            +
                include Fasten::Logger
         | 
| 4 4 | 
             
                include Fasten::DAG
         | 
| 5 5 | 
             
                include Fasten::UI
         | 
| 6 6 | 
             
                include Fasten::LoadSave
         | 
| 7 7 | 
             
                include Fasten::Stats
         | 
| 8 8 |  | 
| 9 | 
            -
                def initialize(name: nil, workers:  | 
| 9 | 
            +
                def initialize(name: nil, developer: STDIN.tty? && STDOUT.tty?, workers: Parallel.physical_processor_count, worker_class: Fasten::Worker, fasten_dir: '.fasten')
         | 
| 10 10 | 
             
                  setup_stats(name)
         | 
| 11 | 
            -
                  super name: name || "#{self.class} #{$PID}", workers: workers, pid: $PID, state: :IDLE, worker_class: worker_class, fasten_dir: fasten_dir
         | 
| 11 | 
            +
                  super name: name || "#{self.class} #{$PID}", workers: workers, pid: $PID, state: :IDLE, worker_class: worker_class, fasten_dir: fasten_dir, developer: developer
         | 
| 12 12 | 
             
                  initialize_dag
         | 
| 13 13 |  | 
| 14 14 | 
             
                  self.worker_list = []
         | 
| @@ -30,7 +30,7 @@ module Fasten | |
| 30 30 | 
             
                  self.state = task_list.map(&:state).all?(:DONE) ? :DONE : :FAIL
         | 
| 31 31 | 
             
                  log_fin self, running_counters
         | 
| 32 32 |  | 
| 33 | 
            -
                  stats_add_entry( | 
| 33 | 
            +
                  stats_add_entry(state, self)
         | 
| 34 34 | 
             
                  save_stats
         | 
| 35 35 | 
             
                end
         | 
| 36 36 |  | 
| @@ -47,46 +47,80 @@ module Fasten | |
| 47 47 | 
             
                    wait_for_running_tasks
         | 
| 48 48 | 
             
                    raise_error_in_failure
         | 
| 49 49 | 
             
                    remove_workers_as_needed
         | 
| 50 | 
            -
                     | 
| 50 | 
            +
                    if %i[PAUSING PAUSED QUITTING].include?(state)
         | 
| 51 | 
            +
                      check_state
         | 
| 52 | 
            +
                    else
         | 
| 53 | 
            +
                      dispatch_pending_tasks
         | 
| 54 | 
            +
                    end
         | 
| 51 55 |  | 
| 52 | 
            -
                    break if no_running_tasks? && no_waiting_tasks?
         | 
| 56 | 
            +
                    break if no_running_tasks? && no_waiting_tasks? || state == :QUIT
         | 
| 53 57 | 
             
                  end
         | 
| 54 58 |  | 
| 55 59 | 
             
                  remove_all_workers
         | 
| 56 60 | 
             
                end
         | 
| 57 61 |  | 
| 62 | 
            +
                def check_state
         | 
| 63 | 
            +
                  if state == :PAUSING && no_running_tasks?
         | 
| 64 | 
            +
                    self.state = :PAUSED
         | 
| 65 | 
            +
                    self.ui.message = nil
         | 
| 66 | 
            +
                    ui.force_clear
         | 
| 67 | 
            +
                  elsif state == :QUITTING && no_running_tasks?
         | 
| 68 | 
            +
                    self.state = :QUIT
         | 
| 69 | 
            +
                    ui.force_clear
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def should_wait_for_running_tasks?
         | 
| 74 | 
            +
                  tasks_running? && (no_waiting_tasks? || tasks_failed? || %i[PAUSING QUITTING].include?(state)) || task_running_list.count >= workers
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 58 77 | 
             
                def wait_for_running_tasks
         | 
| 59 | 
            -
                  while  | 
| 60 | 
            -
                     | 
| 78 | 
            +
                  while should_wait_for_running_tasks?
         | 
| 79 | 
            +
                    ui.update
         | 
| 61 80 | 
             
                    reads = worker_list.map(&:parent_read)
         | 
| 62 | 
            -
                    reads, _writes, _errors = IO.select(reads, [], [],  | 
| 81 | 
            +
                    reads, _writes, _errors = IO.select(reads, [], [], 1)
         | 
| 63 82 |  | 
| 64 83 | 
             
                    receive_workers_tasks(reads)
         | 
| 65 84 | 
             
                  end
         | 
| 66 | 
            -
             | 
| 85 | 
            +
             | 
| 86 | 
            +
                  ui.update
         | 
| 67 87 | 
             
                end
         | 
| 68 88 |  | 
| 69 89 | 
             
                def receive_workers_tasks(reads)
         | 
| 70 90 | 
             
                  reads&.each do |read|
         | 
| 71 | 
            -
                    worker = worker_list.find { |item| item.parent_read == read }
         | 
| 72 | 
            -
             | 
| 91 | 
            +
                    next unless (worker = worker_list.find { |item| item.parent_read == read })
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                    task = worker.receive_response
         | 
| 73 94 |  | 
| 74 95 | 
             
                    task_running_list.delete task
         | 
| 75 96 |  | 
| 76 97 | 
             
                    update_task task
         | 
| 77 98 |  | 
| 78 99 | 
             
                    log_fin task, done_counters
         | 
| 100 | 
            +
                    ui.force_clear
         | 
| 79 101 | 
             
                  end
         | 
| 80 102 | 
             
                end
         | 
| 81 103 |  | 
| 82 104 | 
             
                def raise_error_in_failure
         | 
| 83 105 | 
             
                  return unless tasks_failed?
         | 
| 84 106 |  | 
| 85 | 
            -
                   | 
| 107 | 
            +
                  task_error_list.each do |task|
         | 
| 108 | 
            +
                    log_info "task: #{task} error:#{task.error}\n#{task.error&.backtrace&.join("\n")}"
         | 
| 109 | 
            +
                  end
         | 
| 86 110 |  | 
| 87 | 
            -
                   | 
| 111 | 
            +
                  if developer
         | 
| 112 | 
            +
                    ui.cleanup
         | 
| 113 | 
            +
                    puts "Stopping because the following tasks failed:\n"
         | 
| 114 | 
            +
                    task_error_list.map(&:to_s).each { |x| puts "  #{x}" }
         | 
| 88 115 |  | 
| 89 | 
            -
             | 
| 116 | 
            +
                    puts 'Entering development console'
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                    Kernel.binding.pry # rubocop:disable Lint/Debugger
         | 
| 119 | 
            +
                  else
         | 
| 120 | 
            +
                    remove_all_workers
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                    raise "Stopping because the following tasks failed: #{task_error_list.map(&:to_s).join(', ')}"
         | 
| 123 | 
            +
                  end
         | 
| 90 124 | 
             
                end
         | 
| 91 125 |  | 
| 92 126 | 
             
                def remove_workers_as_needed
         | 
| @@ -95,6 +129,8 @@ module Fasten | |
| 95 129 |  | 
| 96 130 | 
             
                    worker.kill
         | 
| 97 131 | 
             
                    worker_list.delete worker
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                    ui.force_clear
         | 
| 98 134 | 
             
                  end
         | 
| 99 135 | 
             
                end
         | 
| 100 136 |  | 
| @@ -104,10 +140,13 @@ module Fasten | |
| 104 140 | 
             
                  unless worker
         | 
| 105 141 | 
             
                    @worker_id = (@worker_id || 0) + 1
         | 
| 106 142 | 
             
                    worker = worker_class.new executor: self, name: "#{worker_class} #{format '%02X', @worker_id}"
         | 
| 143 | 
            +
                    worker.block = block if block
         | 
| 107 144 | 
             
                    worker.fork
         | 
| 108 145 | 
             
                    worker_list << worker
         | 
| 109 146 |  | 
| 110 147 | 
             
                    log_info "Worker created: #{worker}"
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                    ui.force_clear
         | 
| 111 150 | 
             
                  end
         | 
| 112 151 |  | 
| 113 152 | 
             
                  worker
         | 
| @@ -119,15 +158,18 @@ module Fasten | |
| 119 158 |  | 
| 120 159 | 
             
                    task = next_task
         | 
| 121 160 | 
             
                    log_ini task, "on worker #{worker}"
         | 
| 122 | 
            -
                    worker. | 
| 161 | 
            +
                    worker.send_request(task)
         | 
| 123 162 | 
             
                    task_running_list << task
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                    ui.force_clear
         | 
| 124 165 | 
             
                  end
         | 
| 125 166 | 
             
                end
         | 
| 126 167 |  | 
| 127 168 | 
             
                def remove_all_workers
         | 
| 128 | 
            -
                   | 
| 129 | 
            -
             | 
| 130 | 
            -
             | 
| 169 | 
            +
                  worker_list.each(&:kill)
         | 
| 170 | 
            +
                  worker_list.clear
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                  ui.force_clear
         | 
| 131 173 | 
             
                end
         | 
| 132 174 | 
             
              end
         | 
| 133 175 | 
             
            end
         | 
    
        data/lib/fasten/load_save.rb
    CHANGED
    
    | @@ -4,13 +4,17 @@ module Fasten | |
| 4 4 |  | 
| 5 5 | 
             
                def load(path)
         | 
| 6 6 | 
             
                  items = YAML.safe_load(File.read(path)).each do |name, params|
         | 
| 7 | 
            -
                    params. | 
| 8 | 
            -
                       | 
| 9 | 
            -
             | 
| 10 | 
            -
                      params | 
| 7 | 
            +
                    if params.is_a? String
         | 
| 8 | 
            +
                      params = { after: params }
         | 
| 9 | 
            +
                    else
         | 
| 10 | 
            +
                      params&.each do |key, val|
         | 
| 11 | 
            +
                        next unless val.is_a?(String) && (match = %r{^/(.+)/$}.match(val))
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                        params[key] = Regexp.new(match[1])
         | 
| 14 | 
            +
                      end
         | 
| 11 15 | 
             
                    end
         | 
| 12 16 |  | 
| 13 | 
            -
                    add Fasten::Task.new({ name: name }.merge(params))
         | 
| 17 | 
            +
                    add Fasten::Task.new({ name: name }.merge(params || {}))
         | 
| 14 18 | 
             
                  end
         | 
| 15 19 |  | 
| 16 20 | 
             
                  log_info "Loaded #{items.count} tasks from #{path}"
         | 
| @@ -3,7 +3,7 @@ module Fasten | |
| 3 3 | 
             
                attr_accessor :logger
         | 
| 4 4 | 
             
              end
         | 
| 5 5 |  | 
| 6 | 
            -
              module  | 
| 6 | 
            +
              module Logger
         | 
| 7 7 | 
             
                %w[debug info error].each do |method|
         | 
| 8 8 | 
             
                  define_method "log_#{method}" do |msg|
         | 
| 9 9 | 
             
                    return unless Fasten.logger.respond_to?(method)
         | 
| @@ -21,9 +21,9 @@ module Fasten | |
| 21 21 |  | 
| 22 22 | 
             
                def log_fin(object, message = nil)
         | 
| 23 23 | 
             
                  object.fin ||= Time.new
         | 
| 24 | 
            -
                   | 
| 24 | 
            +
                  object.dif = object.fin - object.ini
         | 
| 25 25 |  | 
| 26 | 
            -
                  log_info "Done #{object.class} #{object} #{message} in #{ | 
| 26 | 
            +
                  log_info "Done #{object.class} #{object} #{message} in #{object.dif}"
         | 
| 27 27 | 
             
                end
         | 
| 28 28 |  | 
| 29 29 | 
             
                def redirect_std(path)
         | 
| @@ -47,7 +47,7 @@ end | |
| 47 47 |  | 
| 48 48 | 
             
            Fasten.logger ||=
         | 
| 49 49 | 
             
              begin
         | 
| 50 | 
            -
                Logger.new(STDOUT, level: Logger:: | 
| 50 | 
            +
                Logger.new(STDOUT, level: Logger::DEBUG, progname: $PROGRAM_NAME)
         | 
| 51 51 | 
             
              rescue StandardError
         | 
| 52 52 | 
             
                nil
         | 
| 53 53 | 
             
              end
         | 
    
        data/lib/fasten/stats.rb
    CHANGED
    
    | @@ -11,21 +11,70 @@ module Fasten | |
| 11 11 | 
             
                  }
         | 
| 12 12 | 
             
                end
         | 
| 13 13 |  | 
| 14 | 
            -
                def stats_add_entry( | 
| 14 | 
            +
                def stats_add_entry(state, target)
         | 
| 15 15 | 
             
                  return unless target.ini && target.fin
         | 
| 16 16 |  | 
| 17 17 | 
             
                  entry = stats_create_entry(state, target)
         | 
| 18 | 
            -
                   | 
| 19 | 
            -
                   | 
| 18 | 
            +
                  self.stats_data ||= []
         | 
| 19 | 
            +
                  self.stats_entries ||= []
         | 
| 20 | 
            +
                  stats_data << entry
         | 
| 21 | 
            +
                  stats_entries << entry
         | 
| 20 22 |  | 
| 21 | 
            -
                  history = stats_history( | 
| 23 | 
            +
                  history = stats_history(entry)
         | 
| 22 24 |  | 
| 25 | 
            +
                  update_cnt(history, entry)
         | 
| 23 26 | 
             
                  update_avg(history, entry)
         | 
| 24 27 | 
             
                  update_std(history, entry)
         | 
| 25 28 | 
             
                end
         | 
| 26 29 |  | 
| 27 | 
            -
                 | 
| 28 | 
            -
             | 
| 30 | 
            +
                FLOAT_FORMATTER = ->(f) { format('%7.3f', f) }
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def stats_table_run
         | 
| 33 | 
            +
                  sub = stats_entries.select { |x| x['kind'] == 'task' }.map { |x| x['run'] }.sum
         | 
| 34 | 
            +
                  tot = stats_entries.select { |x| x['kind'] == 'executor' }.map { |x| x['run'] }.sum
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  [sub, tot]
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def split_time(time)
         | 
| 40 | 
            +
                  sign = time.negative? ? '-' : ''
         | 
| 41 | 
            +
                  time = -time if time.negative?
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  hours, seconds = time.divmod(3600)
         | 
| 44 | 
            +
                  minutes, seconds = seconds.divmod(60)
         | 
| 45 | 
            +
                  seconds, decimal = seconds.divmod(1)
         | 
| 46 | 
            +
                  milliseconds, _ignored = (decimal.round(4) * 1000).divmod(1)
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  [sign, hours, minutes, seconds, milliseconds]
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def hformat(time, total = nil)
         | 
| 52 | 
            +
                  sign, hours, minutes, seconds, milliseconds = split_time time
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  str = hours.zero? ? format('%.1s%02d:%02d.%03d', sign, minutes, seconds, milliseconds) : format('%.1s%02d:%02d:%02d.%03d', sign, hours, minutes, seconds, milliseconds)
         | 
| 55 | 
            +
                  str += format(' (%.1f%%)', 100.0 * time / total) if total
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  str
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def stats_table
         | 
| 61 | 
            +
                  sub, tot = stats_table_run
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  Hirb::Console.render_output(stats_entries,
         | 
| 64 | 
            +
                                              fields: %w[state kind name run cnt avg std], unicode: true, class: 'Hirb::Helpers::AutoTable',
         | 
| 65 | 
            +
                                              filters: { 'run' => FLOAT_FORMATTER, 'avg' => FLOAT_FORMATTER, 'std' => FLOAT_FORMATTER },
         | 
| 66 | 
            +
                                              description: false)
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  puts format('∑tasks: %<task>s ∑executed: %<executed>s saved: %<saved>s workers: %<workers>s',
         | 
| 69 | 
            +
                              task: hformat(sub), executed: hformat(tot, sub), saved: hformat(sub - tot, sub), workers: workers.to_s)
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def stats_history(entry)
         | 
| 73 | 
            +
                  stats_data.select { |e| e['state'] == entry['state'] && e['kind'] == entry['kind'] && e['name'] == entry['name'] }
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def update_cnt(history, entry)
         | 
| 77 | 
            +
                  entry['cnt'] = history.size
         | 
| 29 78 | 
             
                end
         | 
| 30 79 |  | 
| 31 80 | 
             
                def update_avg(history, entry)
         | 
    
        data/lib/fasten/task.rb
    CHANGED
    
    
| @@ -0,0 +1,58 @@ | |
| 1 | 
            +
            require 'forwardable'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Fasten
         | 
| 4 | 
            +
              module UI
         | 
| 5 | 
            +
                class Console
         | 
| 6 | 
            +
                  extend Forwardable
         | 
| 7 | 
            +
                  def_delegators :executor, :worker_list, :task_list, :task_done_list, :task_error_list, :task_running_list, :task_waiting_list, :worker_list
         | 
| 8 | 
            +
                  def_delegators :executor, :name, :workers, :workers=, :state, :state=, :hformat
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  attr_accessor :executor
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def initialize(executor:)
         | 
| 13 | 
            +
                    @executor = executor
         | 
| 14 | 
            +
                    @old = {
         | 
| 15 | 
            +
                      task_done_list: [],
         | 
| 16 | 
            +
                      task_error_list: []
         | 
| 17 | 
            +
                    }
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def setup
         | 
| 21 | 
            +
                    puts <<~FIN
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                      = == === ==== ===== ====== ======= ======== ========= ==========
         | 
| 24 | 
            +
                      Fasten your seatbelts! #{'💺' * workers}
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                      #{name}
         | 
| 27 | 
            +
                    FIN
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    $stdout.sync = true
         | 
| 30 | 
            +
                    @setup_done = true
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  def update
         | 
| 34 | 
            +
                    setup unless @setup_done
         | 
| 35 | 
            +
                    display_task_message(task_done_list, @old[:task_done_list], 'Done in')
         | 
| 36 | 
            +
                    display_task_message(task_error_list, @old[:task_error_list], 'Fail in')
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def cleanup
         | 
| 40 | 
            +
                    puts '========== ========= ======== ======= ====== ===== ==== === == ='
         | 
| 41 | 
            +
                    @setup_done = false
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def force_clear; end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  protected
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  def display_task_message(orig, old, message)
         | 
| 49 | 
            +
                    return unless old.count != orig.count
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    (orig - old).each do |task|
         | 
| 52 | 
            +
                      puts "Time: #{hformat Time.new - executor.ini} #{message} #{hformat task.dif} Task #{task}"
         | 
| 53 | 
            +
                      old << task
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
            end
         | 
| @@ -0,0 +1,256 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'forwardable'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Fasten
         | 
| 6 | 
            +
              module UI
         | 
| 7 | 
            +
                class Curses
         | 
| 8 | 
            +
                  include ::Curses
         | 
| 9 | 
            +
                  extend Forwardable
         | 
| 10 | 
            +
                  def_delegators :executor, :worker_list, :task_list, :task_done_list, :task_error_list, :task_running_list, :task_waiting_list, :worker_list
         | 
| 11 | 
            +
                  def_delegators :executor, :name, :workers, :workers=, :state, :state=
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  attr_accessor :n_rows, :n_cols, :clear_needed, :message, :executor
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  SPINNER_STR = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
         | 
| 16 | 
            +
                  SPINNER_LEN = SPINNER_STR.length
         | 
| 17 | 
            +
                  PROGRESSBAR_STR = ' ▏▎▍▌▋▊▉'
         | 
| 18 | 
            +
                  PROGRESSBAR_LEN = PROGRESSBAR_STR.length
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def initialize(executor:)
         | 
| 21 | 
            +
                    @executor = executor
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def update
         | 
| 25 | 
            +
                    setup unless @setup_done
         | 
| 26 | 
            +
                    ui_keyboard
         | 
| 27 | 
            +
                    clear if clear_needed
         | 
| 28 | 
            +
                    draw_title
         | 
| 29 | 
            +
                    ui_workers
         | 
| 30 | 
            +
                    ui_tasks
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    refresh
         | 
| 33 | 
            +
                    self.clear_needed = false
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def draw_title
         | 
| 37 | 
            +
                    ui_text_aligned(0, :left, 'Fasten your seatbelts!')
         | 
| 38 | 
            +
                    ui_text_aligned(0, :center, name.to_s)
         | 
| 39 | 
            +
                    ui_text_aligned(0, :right, Time.new.to_s)
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  def cleanup
         | 
| 43 | 
            +
                    close_screen
         | 
| 44 | 
            +
                    @setup_done = nil
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  def setup
         | 
| 48 | 
            +
                    init_screen
         | 
| 49 | 
            +
                    self.n_rows = lines
         | 
| 50 | 
            +
                    self.n_cols = cols
         | 
| 51 | 
            +
                    stdscr.keypad = true
         | 
| 52 | 
            +
                    stdscr.nodelay = true
         | 
| 53 | 
            +
                    setup_color
         | 
| 54 | 
            +
                    noecho
         | 
| 55 | 
            +
                    cbreak
         | 
| 56 | 
            +
                    nonl
         | 
| 57 | 
            +
                    curs_set 0
         | 
| 58 | 
            +
                    @setup_done = true
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def setup_color
         | 
| 62 | 
            +
                    start_color
         | 
| 63 | 
            +
                    use_default_colors
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    init_pair 1, Curses::COLOR_YELLOW, -1
         | 
| 66 | 
            +
                    init_pair 2, Curses::COLOR_GREEN, -1
         | 
| 67 | 
            +
                    init_pair 3, Curses::COLOR_RED, -1
         | 
| 68 | 
            +
                    init_pair 4, Curses::COLOR_WHITE, -1
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  def ui_text_aligned(row, align, str, attrs = nil)
         | 
| 72 | 
            +
                    if align == :center
         | 
| 73 | 
            +
                      setpos row, (n_cols - str.length) / 2
         | 
| 74 | 
            +
                    elsif align == :right
         | 
| 75 | 
            +
                      setpos row, n_cols - str.length
         | 
| 76 | 
            +
                    else
         | 
| 77 | 
            +
                      setpos row, 0
         | 
| 78 | 
            +
                    end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                    attrset attrs if attrs
         | 
| 81 | 
            +
                    addstr str
         | 
| 82 | 
            +
                    attroff attrs if attrs
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    str.length
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  def force_clear
         | 
| 88 | 
            +
                    self.clear_needed = true
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  def ui_keyboard
         | 
| 92 | 
            +
                    return unless (key = stdscr.getch)
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                    self.message = nil
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                    if key == Curses::Key::LEFT
         | 
| 97 | 
            +
                      if workers <= 1
         | 
| 98 | 
            +
                        self.message = "Can't remove 1 worker left, press [P] to pause"
         | 
| 99 | 
            +
                      else
         | 
| 100 | 
            +
                        self.workers -= 1
         | 
| 101 | 
            +
                        self.message = "Decreasing max workers to #{workers}"
         | 
| 102 | 
            +
                      end
         | 
| 103 | 
            +
                    elsif key == Curses::Key::RIGHT
         | 
| 104 | 
            +
                      self.workers += 1
         | 
| 105 | 
            +
                      self.message = "Increasing max workers to #{workers}"
         | 
| 106 | 
            +
                    elsif key == 'q'
         | 
| 107 | 
            +
                      self.message = 'Will quit when running tasks end'
         | 
| 108 | 
            +
                      self.state = :QUITTING
         | 
| 109 | 
            +
                    elsif key == 'p'
         | 
| 110 | 
            +
                      self.message = 'Will pause when running tasks end'
         | 
| 111 | 
            +
                      self.state = :PAUSING
         | 
| 112 | 
            +
                    elsif key == 'r'
         | 
| 113 | 
            +
                      self.state = :RUNNING
         | 
| 114 | 
            +
                    end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                    force_clear
         | 
| 117 | 
            +
                  end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  def ui_workers_summary
         | 
| 120 | 
            +
                    running_count = task_running_list.count
         | 
| 121 | 
            +
                    waiting_count = task_waiting_list.count
         | 
| 122 | 
            +
                    workers_count = worker_list.count
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    "Procs: #{running_count} run #{workers_count - running_count} idle #{workers} max #{waiting_count} wait"
         | 
| 125 | 
            +
                  end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                  def ui_workers
         | 
| 128 | 
            +
                    l = ui_text_aligned(1, :left, ui_workers_summary) + 1
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                    worker_list.each_with_index do |worker, index|
         | 
| 131 | 
            +
                      setpos 1, l + index
         | 
| 132 | 
            +
                      attrs = worker.running? ? A_STANDOUT : color_pair(4) | A_DIM
         | 
| 133 | 
            +
                      attrset attrs
         | 
| 134 | 
            +
                      addstr worker.running? ? 'R' : '_'
         | 
| 135 | 
            +
                      attroff attrs
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                    ui_state
         | 
| 139 | 
            +
                  end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                  def ui_state
         | 
| 142 | 
            +
                    if state == :RUNNING
         | 
| 143 | 
            +
                      attrs = color_pair(2)
         | 
| 144 | 
            +
                    elsif state == :PAUSING
         | 
| 145 | 
            +
                      attrs = color_pair(1) | A_BLINK | A_STANDOUT
         | 
| 146 | 
            +
                    elsif state == :PAUSED
         | 
| 147 | 
            +
                      attrs = color_pair(1) | A_STANDOUT
         | 
| 148 | 
            +
                    elsif state == :QUITTING
         | 
| 149 | 
            +
                      attrs = color_pair(3) | A_BLINK | A_STANDOUT
         | 
| 150 | 
            +
                    end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                    l = ui_text_aligned(1, :right, state.to_s, attrs)
         | 
| 153 | 
            +
                    return unless message
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                    setpos 1, n_cols - l - message.length - 1
         | 
| 156 | 
            +
                    addstr message
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                  def ui_progressbar(row, col_ini, col_fin, count, total)
         | 
| 160 | 
            +
                    slice = total.to_f / (col_fin - col_ini + 1)
         | 
| 161 | 
            +
                    col_ini.upto col_fin do |col|
         | 
| 162 | 
            +
                      setpos row, col
         | 
| 163 | 
            +
                      count -= slice
         | 
| 164 | 
            +
                      if count.positive?
         | 
| 165 | 
            +
                        addstr PROGRESSBAR_STR[-1]
         | 
| 166 | 
            +
                      elsif count > -slice
         | 
| 167 | 
            +
                        addstr PROGRESSBAR_STR[(count * PROGRESSBAR_LEN / slice) % PROGRESSBAR_LEN]
         | 
| 168 | 
            +
                      else
         | 
| 169 | 
            +
                        addstr '.'
         | 
| 170 | 
            +
                      end
         | 
| 171 | 
            +
                    end
         | 
| 172 | 
            +
                  end
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                  def ui_task_icon(task)
         | 
| 175 | 
            +
                    case task.state
         | 
| 176 | 
            +
                    when :RUNNING
         | 
| 177 | 
            +
                      SPINNER_STR[task.worker&.spinner]
         | 
| 178 | 
            +
                    when :FAIL
         | 
| 179 | 
            +
                      '✘'
         | 
| 180 | 
            +
                    when :DONE
         | 
| 181 | 
            +
                      '✔'
         | 
| 182 | 
            +
                    else
         | 
| 183 | 
            +
                      '…'
         | 
| 184 | 
            +
                    end
         | 
| 185 | 
            +
                  end
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                  def ui_task_color(task)
         | 
| 188 | 
            +
                    case task.state
         | 
| 189 | 
            +
                    when :RUNNING
         | 
| 190 | 
            +
                      color_pair(1) | A_TOP
         | 
| 191 | 
            +
                    when :FAIL
         | 
| 192 | 
            +
                      color_pair(3) | A_TOP
         | 
| 193 | 
            +
                    when :DONE
         | 
| 194 | 
            +
                      color_pair(2) | A_TOP
         | 
| 195 | 
            +
                    else
         | 
| 196 | 
            +
                      color_pair(4) | A_TOP
         | 
| 197 | 
            +
                    end
         | 
| 198 | 
            +
                  end
         | 
| 199 | 
            +
             | 
| 200 | 
            +
                  def ui_task_string(task, y, x, icon: nil, str: nil)
         | 
| 201 | 
            +
                    setpos y, x
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                    attrs = ui_task_color(task)
         | 
| 204 | 
            +
                    icon = ui_task_icon(task) if icon
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                    str ||= icon ? "#{icon} #{task}" : task.to_s
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                    attrset attrs if attrs
         | 
| 209 | 
            +
                    addstr str
         | 
| 210 | 
            +
                    attroff attrs if attrs
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                    x + str.length
         | 
| 213 | 
            +
                  end
         | 
| 214 | 
            +
             | 
| 215 | 
            +
                  def ui_tasks
         | 
| 216 | 
            +
                    worker_list.each do |worker|
         | 
| 217 | 
            +
                      worker.spinner = (worker.spinner + 1) % SPINNER_LEN if worker.running?
         | 
| 218 | 
            +
                    end
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                    count_done = task_done_list.count
         | 
| 221 | 
            +
                    count_total = task_list.count
         | 
| 222 | 
            +
                    tl = count_total.to_s.length
         | 
| 223 | 
            +
                    col_ini = ui_text_aligned(2, :left, format("Tasks: %#{tl}d/%d", count_done, count_total)) + 1
         | 
| 224 | 
            +
                    col_fin = n_cols - 5
         | 
| 225 | 
            +
                    ui_text_aligned(2, :right, "#{(count_done * 100 / count_total).to_i}%") if count_total.positive?
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                    ui_progressbar(2, col_ini, col_fin, count_done, count_total)
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                    max = 2
         | 
| 230 | 
            +
                    list = task_list.sort_by(&:run_score)
         | 
| 231 | 
            +
                    list.each_with_index do |task, index|
         | 
| 232 | 
            +
                      next if 3 + index >= n_rows
         | 
| 233 | 
            +
             | 
| 234 | 
            +
                      x = ui_task_string(task, 3 + index, 2, icon: true)
         | 
| 235 | 
            +
                      max = x if x > max
         | 
| 236 | 
            +
                    end
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                    list.each_with_index do |task, index|
         | 
| 239 | 
            +
                      next if 3 + index >= n_rows
         | 
| 240 | 
            +
             | 
| 241 | 
            +
                      if task.dif
         | 
| 242 | 
            +
                        setpos 3 + index, max + 2
         | 
| 243 | 
            +
                        ui_task_string(task, 3 + index, max + 2, str: format('%.2f s', task.dif))
         | 
| 244 | 
            +
                      elsif task.depends && !task.depends.empty?
         | 
| 245 | 
            +
                        setpos 3 + index, max
         | 
| 246 | 
            +
                        x = max + 2
         | 
| 247 | 
            +
                        addstr ':'
         | 
| 248 | 
            +
                        task.depends.each do |dependant_task|
         | 
| 249 | 
            +
                          x = ui_task_string(dependant_task, 3 + index, x) + 1
         | 
| 250 | 
            +
                        end
         | 
| 251 | 
            +
                      end
         | 
| 252 | 
            +
                    end
         | 
| 253 | 
            +
                  end
         | 
| 254 | 
            +
                end
         | 
| 255 | 
            +
              end
         | 
| 256 | 
            +
            end
         | 
    
        data/lib/fasten/ui.rb
    CHANGED
    
    | @@ -1,166 +1,18 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            require 'fasten/ui/console'
         | 
| 2 | 
            +
            require 'fasten/ui/curses'
         | 
| 2 3 |  | 
| 3 4 | 
             
            module Fasten
         | 
| 4 5 | 
             
              module UI
         | 
| 5 | 
            -
                 | 
| 6 | 
            -
             | 
| 7 | 
            -
                 | 
| 8 | 
            -
                SPINNER_LEN = SPINNER_STR.length
         | 
| 9 | 
            -
                PROGRESSBAR_STR = ' ▏▎▍▌▋▊▉'
         | 
| 10 | 
            -
                PROGRESSBAR_LEN = PROGRESSBAR_STR.length
         | 
| 6 | 
            +
                def ui
         | 
| 7 | 
            +
                  @ui ||= STDIN.tty? && STDOUT.tty? ? Fasten::UI::Curses.new(executor: self) : Fasten::UI::Console.new(executor: self)
         | 
| 8 | 
            +
                end
         | 
| 11 9 |  | 
| 12 10 | 
             
                def run_ui
         | 
| 13 | 
            -
                   | 
| 14 | 
            -
                  ui_title
         | 
| 15 | 
            -
                  ui_workers
         | 
| 11 | 
            +
                  ui.update
         | 
| 16 12 |  | 
| 17 13 | 
             
                  yield
         | 
| 18 14 | 
             
                ensure
         | 
| 19 | 
            -
                   | 
| 20 | 
            -
                end
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                def ui_setup
         | 
| 23 | 
            -
                  init_screen
         | 
| 24 | 
            -
                  self.ui_rows = lines
         | 
| 25 | 
            -
                  self.ui_cols = cols
         | 
| 26 | 
            -
                  ui_setup_color
         | 
| 27 | 
            -
                  noecho
         | 
| 28 | 
            -
                  cbreak
         | 
| 29 | 
            -
                  nonl
         | 
| 30 | 
            -
                  curs_set 0
         | 
| 31 | 
            -
                end
         | 
| 32 | 
            -
             | 
| 33 | 
            -
                def ui_setup_color
         | 
| 34 | 
            -
                  start_color
         | 
| 35 | 
            -
                  use_default_colors
         | 
| 36 | 
            -
                  init_pair 1, Curses::COLOR_YELLOW, -1
         | 
| 37 | 
            -
                  init_pair 2, Curses::COLOR_GREEN, -1
         | 
| 38 | 
            -
                  init_pair 3, Curses::COLOR_RED, -1
         | 
| 39 | 
            -
                  init_pair 4, Curses::COLOR_WHITE, -1
         | 
| 40 | 
            -
                end
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                def ui_text_aligned(row, align, str, attrs = nil)
         | 
| 43 | 
            -
                  if align == :center
         | 
| 44 | 
            -
                    setpos row, (ui_cols - str.length) / 2
         | 
| 45 | 
            -
                  elsif align == :right
         | 
| 46 | 
            -
                    setpos row, ui_cols - str.length
         | 
| 47 | 
            -
                  else
         | 
| 48 | 
            -
                    setpos row, 0
         | 
| 49 | 
            -
                  end
         | 
| 50 | 
            -
             | 
| 51 | 
            -
                  attrset attrs if attrs
         | 
| 52 | 
            -
                  addstr str
         | 
| 53 | 
            -
                  attroff attrs if attrs
         | 
| 54 | 
            -
             | 
| 55 | 
            -
                  str.length
         | 
| 56 | 
            -
                end
         | 
| 57 | 
            -
             | 
| 58 | 
            -
                def ui_title
         | 
| 59 | 
            -
                  ui_text_aligned(0, :left, 'Fasten your seatbelts!')
         | 
| 60 | 
            -
                  ui_text_aligned(0, :center, name.to_s)
         | 
| 61 | 
            -
                  ui_text_aligned(0, :right, Time.new.to_s)
         | 
| 62 | 
            -
                end
         | 
| 63 | 
            -
             | 
| 64 | 
            -
                def ui_update
         | 
| 65 | 
            -
                  clear
         | 
| 66 | 
            -
                  ui_title
         | 
| 67 | 
            -
                  ui_workers
         | 
| 68 | 
            -
                  ui_tasks
         | 
| 69 | 
            -
             | 
| 70 | 
            -
                  refresh
         | 
| 71 | 
            -
                end
         | 
| 72 | 
            -
             | 
| 73 | 
            -
                def ui_workers_summary
         | 
| 74 | 
            -
                  "Procs: #{task_running_list.count} run #{worker_list.count - task_running_list.count} idle #{workers} max"
         | 
| 75 | 
            -
                end
         | 
| 76 | 
            -
             | 
| 77 | 
            -
                def ui_workers
         | 
| 78 | 
            -
                  l = ui_text_aligned(1, :left, ui_workers_summary) + 1
         | 
| 79 | 
            -
             | 
| 80 | 
            -
                  worker_list.each_with_index do |worker, index|
         | 
| 81 | 
            -
                    setpos 1, l + index
         | 
| 82 | 
            -
                    attrs = worker.running? ? A_STANDOUT : color_pair(4) | A_DIM
         | 
| 83 | 
            -
                    attrset attrs
         | 
| 84 | 
            -
                    addstr worker.running? ? 'R' : '_'
         | 
| 85 | 
            -
                    attroff attrs
         | 
| 86 | 
            -
                  end
         | 
| 87 | 
            -
                end
         | 
| 88 | 
            -
             | 
| 89 | 
            -
                def ui_progressbar(row, col_ini, col_fin, count, total)
         | 
| 90 | 
            -
                  slice = total.to_f / (col_fin - col_ini + 1)
         | 
| 91 | 
            -
                  col_ini.upto col_fin do |col|
         | 
| 92 | 
            -
                    setpos row, col
         | 
| 93 | 
            -
                    count -= slice
         | 
| 94 | 
            -
                    if count.positive?
         | 
| 95 | 
            -
                      addstr PROGRESSBAR_STR[-1]
         | 
| 96 | 
            -
                    elsif count > -slice
         | 
| 97 | 
            -
                      addstr PROGRESSBAR_STR[(count * PROGRESSBAR_LEN / slice) % PROGRESSBAR_LEN]
         | 
| 98 | 
            -
                    else
         | 
| 99 | 
            -
                      addstr '.'
         | 
| 100 | 
            -
                    end
         | 
| 101 | 
            -
                  end
         | 
| 102 | 
            -
                end
         | 
| 103 | 
            -
             | 
| 104 | 
            -
                def ui_task_string(task, y, x, icon: nil)
         | 
| 105 | 
            -
                  setpos y, x
         | 
| 106 | 
            -
             | 
| 107 | 
            -
                  case task.state
         | 
| 108 | 
            -
                  when :RUNNING
         | 
| 109 | 
            -
                    attrs = color_pair(1) | A_TOP
         | 
| 110 | 
            -
                    icon = SPINNER_STR[task.worker&.spinner] if icon
         | 
| 111 | 
            -
                  when :FAIL
         | 
| 112 | 
            -
                    attrs = color_pair(3)
         | 
| 113 | 
            -
                    icon = '✘︎' if icon
         | 
| 114 | 
            -
                  when :DONE
         | 
| 115 | 
            -
                    attrs = color_pair(2)
         | 
| 116 | 
            -
                    icon = '✔︎' if icon
         | 
| 117 | 
            -
                  else
         | 
| 118 | 
            -
                    attrs = color_pair(4) | A_DIM
         | 
| 119 | 
            -
                    icon = '…' if icon
         | 
| 120 | 
            -
                  end
         | 
| 121 | 
            -
             | 
| 122 | 
            -
                  str = icon ? "#{icon} #{task}" : task.to_s
         | 
| 123 | 
            -
             | 
| 124 | 
            -
                  attrset attrs if attrs
         | 
| 125 | 
            -
                  addstr str
         | 
| 126 | 
            -
                  attroff attrs if attrs
         | 
| 127 | 
            -
             | 
| 128 | 
            -
                  x + str.length
         | 
| 129 | 
            -
                end
         | 
| 130 | 
            -
             | 
| 131 | 
            -
                def ui_tasks
         | 
| 132 | 
            -
                  worker_list.each do |worker|
         | 
| 133 | 
            -
                    worker.spinner = (worker.spinner + 1) % SPINNER_LEN if worker.running?
         | 
| 134 | 
            -
                  end
         | 
| 135 | 
            -
             | 
| 136 | 
            -
                  count_done = task_done_list.count
         | 
| 137 | 
            -
                  count_total = task_list.count
         | 
| 138 | 
            -
                  tl = count_total.to_s.length
         | 
| 139 | 
            -
                  col_ini = ui_text_aligned(2, :left, format("Tasks: %#{tl}d/%s", count_done, count_total)) + 1
         | 
| 140 | 
            -
                  col_fin = ui_cols - 5
         | 
| 141 | 
            -
                  ui_text_aligned(2, :right, "#{(count_done * 100/count_total).to_i}%") if count_total.positive?
         | 
| 142 | 
            -
             | 
| 143 | 
            -
                  ui_progressbar(2, col_ini, col_fin, count_done, count_total)
         | 
| 144 | 
            -
             | 
| 145 | 
            -
                  max = 2
         | 
| 146 | 
            -
                  list = task_list.sort_by(&:run_score)
         | 
| 147 | 
            -
                  list.each_with_index do |task, index|
         | 
| 148 | 
            -
                    next if 3 + index >= ui_rows
         | 
| 149 | 
            -
             | 
| 150 | 
            -
                    x = ui_task_string(task, 3 + index, 2, icon: true)
         | 
| 151 | 
            -
                    max = x if x > max
         | 
| 152 | 
            -
                  end
         | 
| 153 | 
            -
             | 
| 154 | 
            -
                  list.each_with_index do |task, index|
         | 
| 155 | 
            -
                    next if 3 + index >= ui_rows || task.depends.nil? || task.depends.empty?
         | 
| 156 | 
            -
             | 
| 157 | 
            -
                    setpos 3 + index, max
         | 
| 158 | 
            -
                    x = max + 2
         | 
| 159 | 
            -
                    addstr ':'
         | 
| 160 | 
            -
                    task.depends.each do |dependant_task|
         | 
| 161 | 
            -
                      x = ui_task_string(dependant_task, 3 + index, x) + 1
         | 
| 162 | 
            -
                    end
         | 
| 163 | 
            -
                  end
         | 
| 15 | 
            +
                  ui.cleanup
         | 
| 164 16 | 
             
                end
         | 
| 165 17 | 
             
              end
         | 
| 166 18 | 
             
            end
         | 
    
        data/lib/fasten/version.rb
    CHANGED
    
    
    
        data/lib/fasten/worker.rb
    CHANGED
    
    | @@ -1,13 +1,38 @@ | |
| 1 1 | 
             
            module Fasten
         | 
| 2 | 
            +
              class WorkerError < StandardError
         | 
| 3 | 
            +
                attr_reader :backtrace
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(origin)
         | 
| 6 | 
            +
                  super "#{origin.class} #{origin.message}"
         | 
| 7 | 
            +
                  @backtrace = origin.backtrace
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
             | 
| 2 11 | 
             
              class Worker < Task
         | 
| 3 | 
            -
                include Fasten:: | 
| 12 | 
            +
                include Fasten::Logger
         | 
| 4 13 |  | 
| 5 14 | 
             
                def initialize(executor:, name: nil)
         | 
| 6 15 | 
             
                  super executor: executor, name: name, spinner: 0
         | 
| 7 16 | 
             
                end
         | 
| 8 17 |  | 
| 9 18 | 
             
                def perform(task)
         | 
| 10 | 
            -
                   | 
| 19 | 
            +
                  perform_shell(task) if task.shell
         | 
| 20 | 
            +
                  perform_ruby(task) if task.ruby
         | 
| 21 | 
            +
                  perform_block(task) if block
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def perform_ruby(task)
         | 
| 25 | 
            +
                  task.response = eval task.ruby # rubocop:disable Security/Eval we trust our users ;-)
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def perform_shell(task)
         | 
| 29 | 
            +
                  result = system task.shell
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  raise "Command failed with exit code: #{$CHILD_STATUS.exitstatus}" unless result
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def perform_block(task)
         | 
| 35 | 
            +
                  task.response = block.call(task.request)
         | 
| 11 36 | 
             
                end
         | 
| 12 37 |  | 
| 13 38 | 
             
                def fork
         | 
| @@ -16,20 +41,20 @@ module Fasten | |
| 16 41 | 
             
                  self.pid = Process.fork do
         | 
| 17 42 | 
             
                    close_parent_pipes
         | 
| 18 43 |  | 
| 19 | 
            -
                     | 
| 44 | 
            +
                    process_incoming_requests
         | 
| 20 45 | 
             
                  end
         | 
| 21 46 |  | 
| 22 47 | 
             
                  close_child_pipes
         | 
| 23 48 | 
             
                end
         | 
| 24 49 |  | 
| 25 | 
            -
                def  | 
| 50 | 
            +
                def send_request(task)
         | 
| 26 51 | 
             
                  Marshal.dump(Task.new(task.to_h.merge(depends: nil, dependants: nil)), parent_write)
         | 
| 27 52 | 
             
                  self.running_task = task
         | 
| 28 53 | 
             
                  task.worker = self
         | 
| 29 54 | 
             
                  task.state = :RUNNING
         | 
| 30 55 | 
             
                end
         | 
| 31 56 |  | 
| 32 | 
            -
                def  | 
| 57 | 
            +
                def receive_response
         | 
| 33 58 | 
             
                  updated_task = Marshal.load(parent_read) # rubocop:disable Security/MarshalLoad because pipe is a secure channel
         | 
| 34 59 |  | 
| 35 60 | 
             
                  %i[ini fin response error].each { |key| running_task[key] = updated_task[key] }
         | 
| @@ -74,14 +99,14 @@ module Fasten | |
| 74 99 | 
             
                  child_write.close unless child_write.closed?
         | 
| 75 100 | 
             
                end
         | 
| 76 101 |  | 
| 77 | 
            -
                def  | 
| 78 | 
            -
                  log_ini self, ' | 
| 102 | 
            +
                def process_incoming_requests
         | 
| 103 | 
            +
                  log_ini self, 'process_incoming_requests'
         | 
| 79 104 |  | 
| 80 105 | 
             
                  while (object = Marshal.load(child_read)) # rubocop:disable Security/MarshalLoad because pipe is a secure channel
         | 
| 81 106 | 
             
                    run_task(object) if object.is_a? Fasten::Task
         | 
| 82 107 | 
             
                  end
         | 
| 83 108 |  | 
| 84 | 
            -
                  log_fin self, ' | 
| 109 | 
            +
                  log_fin self, 'process_incoming_requests'
         | 
| 85 110 | 
             
                rescue EOFError
         | 
| 86 111 | 
             
                  log_info 'Terminating on EOF'
         | 
| 87 112 | 
             
                end
         | 
| @@ -102,12 +127,17 @@ module Fasten | |
| 102 127 | 
             
                  log_ini task, 'perform'
         | 
| 103 128 |  | 
| 104 129 | 
             
                  perform(task)
         | 
| 105 | 
            -
             | 
| 106 | 
            -
                  log_fin task, 'perform'
         | 
| 107 | 
            -
                  Marshal.dump(task, child_write)
         | 
| 108 130 | 
             
                rescue StandardError => error
         | 
| 109 | 
            -
                  task.error = error
         | 
| 110 | 
            -
             | 
| 131 | 
            +
                  task.error = WorkerError.new(error)
         | 
| 132 | 
            +
                ensure
         | 
| 133 | 
            +
                  log_fin task, 'perform'
         | 
| 134 | 
            +
                  send_response(task)
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                def send_response(task)
         | 
| 138 | 
            +
                  log_info "Sending task response back to executor #{task}"
         | 
| 139 | 
            +
                  data = Marshal.dump(task)
         | 
| 140 | 
            +
                  child_write.write(data)
         | 
| 111 141 | 
             
                end
         | 
| 112 142 | 
             
              end
         | 
| 113 143 | 
             
            end
         | 
    
        data/lib/fasten.rb
    CHANGED
    
    | @@ -8,8 +8,10 @@ require 'ostruct' | |
| 8 8 | 
             
            require 'curses'
         | 
| 9 9 | 
             
            require 'fileutils'
         | 
| 10 10 | 
             
            require 'csv'
         | 
| 11 | 
            +
            require 'hirb'
         | 
| 12 | 
            +
            require 'parallel'
         | 
| 11 13 |  | 
| 12 | 
            -
            require 'fasten/ | 
| 14 | 
            +
            require 'fasten/logger'
         | 
| 13 15 | 
             
            require 'fasten/stats'
         | 
| 14 16 | 
             
            require 'fasten/task'
         | 
| 15 17 | 
             
            require 'fasten/ui'
         | 
| @@ -21,7 +23,7 @@ require 'fasten/version' | |
| 21 23 |  | 
| 22 24 | 
             
            module Fasten
         | 
| 23 25 | 
             
              class << self
         | 
| 24 | 
            -
                include Fasten:: | 
| 26 | 
            +
                include Fasten::Logger
         | 
| 25 27 |  | 
| 26 28 | 
             
                def load(path, **options)
         | 
| 27 29 | 
             
                  executor = Fasten::Executor.new(**options)
         | 
| @@ -29,5 +31,19 @@ module Fasten | |
| 29 31 |  | 
| 30 32 | 
             
                  executor
         | 
| 31 33 | 
             
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def map(list, **options, &block)
         | 
| 36 | 
            +
                  executor = Fasten::Executor.new(**options)
         | 
| 37 | 
            +
                  executor.block = block
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  list.each do |item|
         | 
| 40 | 
            +
                    executor.add Fasten::Task.new name: item.to_s, request: item
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  executor.perform
         | 
| 44 | 
            +
                  executor.stats_table
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  executor.task_list.map(&:response)
         | 
| 47 | 
            +
                end
         | 
| 32 48 | 
             
              end
         | 
| 33 49 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: fasten
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.5.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Aldrin Martoq
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2018-10- | 
| 11 | 
            +
            date: 2018-10-25 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: bundler
         | 
| @@ -108,6 +108,34 @@ dependencies: | |
| 108 108 | 
             
                - - ">="
         | 
| 109 109 | 
             
                  - !ruby/object:Gem::Version
         | 
| 110 110 | 
             
                    version: '0'
         | 
| 111 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 112 | 
            +
              name: hirb
         | 
| 113 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 114 | 
            +
                requirements:
         | 
| 115 | 
            +
                - - ">="
         | 
| 116 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 117 | 
            +
                    version: '0'
         | 
| 118 | 
            +
              type: :runtime
         | 
| 119 | 
            +
              prerelease: false
         | 
| 120 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 121 | 
            +
                requirements:
         | 
| 122 | 
            +
                - - ">="
         | 
| 123 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 124 | 
            +
                    version: '0'
         | 
| 125 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 126 | 
            +
              name: parallel
         | 
| 127 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 128 | 
            +
                requirements:
         | 
| 129 | 
            +
                - - ">="
         | 
| 130 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 131 | 
            +
                    version: '0'
         | 
| 132 | 
            +
              type: :runtime
         | 
| 133 | 
            +
              prerelease: false
         | 
| 134 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 135 | 
            +
                requirements:
         | 
| 136 | 
            +
                - - ">="
         | 
| 137 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 138 | 
            +
                    version: '0'
         | 
| 111 139 | 
             
            description: Fasten your seatbelts! Run jobs in parallel, intelligently.
         | 
| 112 140 | 
             
            email:
         | 
| 113 141 | 
             
            - contacto@a0.cl
         | 
| @@ -134,10 +162,12 @@ files: | |
| 134 162 | 
             
            - lib/fasten/dag.rb
         | 
| 135 163 | 
             
            - lib/fasten/executor.rb
         | 
| 136 164 | 
             
            - lib/fasten/load_save.rb
         | 
| 137 | 
            -
            - lib/fasten/ | 
| 165 | 
            +
            - lib/fasten/logger.rb
         | 
| 138 166 | 
             
            - lib/fasten/stats.rb
         | 
| 139 167 | 
             
            - lib/fasten/task.rb
         | 
| 140 168 | 
             
            - lib/fasten/ui.rb
         | 
| 169 | 
            +
            - lib/fasten/ui/console.rb
         | 
| 170 | 
            +
            - lib/fasten/ui/curses.rb
         | 
| 141 171 | 
             
            - lib/fasten/version.rb
         | 
| 142 172 | 
             
            - lib/fasten/worker.rb
         | 
| 143 173 | 
             
            homepage: https://github.com/a0/fasten/
         |