plywood 1.1.1
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 +7 -0
- data/README.md +47 -0
- data/lib/plywood/color.rb +38 -0
- data/lib/plywood/command.rb +36 -0
- data/lib/plywood/command_report.rb +32 -0
- data/lib/plywood/commands.rb +108 -0
- data/lib/plywood/fancy_logger/fancy_log.rb +55 -0
- data/lib/plywood/fancy_logger/frame.rb +44 -0
- data/lib/plywood/fancy_logger.rb +150 -0
- data/lib/plywood/io_handler.rb +19 -0
- data/lib/plywood/logger.rb +79 -0
- data/lib/plywood/named_io.rb +31 -0
- data/lib/plywood/version.rb +3 -0
- data/lib/plywood.rb +5 -0
- data/logo.jpeg +0 -0
- metadata +173 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9cb35cf97e0f00639e9c3ec01c009100eef846ae77c245daeb0f80f124f3d59d
|
4
|
+
data.tar.gz: eb0c36061977ab643b328daed9b33b569ee78df08c30594f4a93c9bccf024e55
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3b5c5fb7d6f88f80ca2d46ffa2566eda56adf76e20128e87dfe02f4a2c1bcff34e88260b3f9c12377205638265031456dc6846a9c1ed552668f5dab608c2d45e
|
7
|
+
data.tar.gz: de6f5e1b9ac405eaf3322ed9c35ce1b9e5d3aaeb04d06a811cf9ac9e32a60eb6b2e1135e6ce4a38bf94318909bc3e3d4ea16e9acf80d8b290ad128a470789e4d
|
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+

|
2
|
+
|
3
|
+
# Plywood
|
4
|
+
|
5
|
+
A simple way to get multiplexing; run commands concurrently, combine their output streams.
|
6
|
+
|
7
|
+
# Usage
|
8
|
+
|
9
|
+
Typically you feed it an array of items; Plywood yields the items one by one and expects you to make a command using that thing.
|
10
|
+
|
11
|
+
It then runs all the commands at the same time.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
require "plywood"
|
15
|
+
|
16
|
+
items = %w[foo bar baz]
|
17
|
+
|
18
|
+
# There is no need to do this concurrently but this is just an example
|
19
|
+
Plywood::Commands.map(items) do |item|
|
20
|
+
"touch #{item}.txt" # the command that should be run
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
Plywood, by default, will use `item.to_s` to use as the "process identifier". This identifier must be unique; and you are responsible for that. Often this is not a problem at all, say when you loop over a bunch of hostnames or chemical elements, but when you loop over a group of people it's very well possible that two of them share the same name.
|
25
|
+
|
26
|
+
To help with that you can specify the `id` parameter. If it's a `Symbol` Plywood will assume it's the name of a method on the item with arity 0 and call it. The return value is used as the process identifier. If it's a `Proc`; Plywood will call it with the item as the only argument.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
my_favorite_things = [
|
30
|
+
Thing.new("raindrops on roses"),
|
31
|
+
Thing.new("whiskers on kittens"),
|
32
|
+
Thing.new("bright copper kettles"),
|
33
|
+
Thing.new("warm woolen mittens"),
|
34
|
+
Thing.new("brown paper packages tied up with strings")]
|
35
|
+
|
36
|
+
Plywood::Commands.map(my_favorite_things, id: :object_id) do |thing|
|
37
|
+
"touch #{thing.underscore}.txt"
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
You can disable concurrent execution by using `sequential: true`:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
Plywood::Commands.map(my_favorite_things, sequential: true) do |thing|
|
45
|
+
"touch #{thing.underscore}.txt"
|
46
|
+
end
|
47
|
+
```
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Plywood
|
2
|
+
module Color
|
3
|
+
ANSI = {
|
4
|
+
reset: 0,
|
5
|
+
black: 30,
|
6
|
+
red: 31,
|
7
|
+
green: 32,
|
8
|
+
yellow: 33,
|
9
|
+
blue: 34,
|
10
|
+
magenta: 35,
|
11
|
+
cyan: 36,
|
12
|
+
white: 37,
|
13
|
+
bright_black: 30,
|
14
|
+
bright_red: 31,
|
15
|
+
bright_green: 32,
|
16
|
+
bright_yellow: 33,
|
17
|
+
bright_blue: 34,
|
18
|
+
bright_magenta: 35,
|
19
|
+
bright_cyan: 36,
|
20
|
+
bright_white: 37 }.freeze
|
21
|
+
|
22
|
+
def self.enable(io)
|
23
|
+
io.extend(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
def color?
|
27
|
+
return false unless respond_to?(:isatty)
|
28
|
+
isatty && ENV["TERM"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def color(name)
|
32
|
+
return "" unless color?
|
33
|
+
ansi = ANSI[name.to_sym]
|
34
|
+
return "" if ansi.nil?
|
35
|
+
"\e[#{ansi}m"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative "named_io"
|
2
|
+
|
3
|
+
module Plywood
|
4
|
+
class Command
|
5
|
+
attr_reader :command, :name
|
6
|
+
attr_reader :err, :out, :pid
|
7
|
+
|
8
|
+
def initialize(name, command)
|
9
|
+
@name = name.to_s
|
10
|
+
@command = command.to_s
|
11
|
+
@err, @err_writer = build_pipe(:err)
|
12
|
+
@out, @out_writer = build_pipe(:out)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"#{name} `#{@command}`"
|
17
|
+
end
|
18
|
+
|
19
|
+
def ios
|
20
|
+
[err, out]
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
@pid = ::Process.spawn(@command, err: @err_writer, out: @out_writer)
|
25
|
+
@err_writer.puts "started with pid #{@pid}"
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def build_pipe(stream)
|
32
|
+
reader, writer = IO.pipe
|
33
|
+
[NamedIO.new(reader, @name, stream), writer]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
require_relative "command"
|
3
|
+
|
4
|
+
module Plywood
|
5
|
+
# A small report to show after the command has run.
|
6
|
+
class CommandReport
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :@command, :command, :name, :pid
|
10
|
+
def_delegators :@status, :success?, :to_i
|
11
|
+
|
12
|
+
def initialize(command, status)
|
13
|
+
raise ArgumentError, "command is nil" if command.nil?
|
14
|
+
raise ArgumentError, "status is nil" if status.nil?
|
15
|
+
@command = command
|
16
|
+
@status = status
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.gather(status, commands)
|
20
|
+
command = commands.find { |item| item.pid == status.pid }
|
21
|
+
new(command, status)
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
if success?
|
26
|
+
"#{name} was successful."
|
27
|
+
else
|
28
|
+
"#{name} failed with exit status #{to_i} (`#{command}`)."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require_relative "command"
|
2
|
+
require_relative "command_report"
|
3
|
+
require_relative "logger"
|
4
|
+
|
5
|
+
module Plywood
|
6
|
+
class Commands
|
7
|
+
def initialize(io_handler_class: Logger, sequential: false)
|
8
|
+
@list = {}
|
9
|
+
@io_handler_class = io_handler_class
|
10
|
+
@sequential = sequential
|
11
|
+
yield self
|
12
|
+
run
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.map(array, io_handler_class: Logger, id: :to_s, sequential: false)
|
16
|
+
new(io_handler_class: io_handler_class, sequential: sequential) do |ply|
|
17
|
+
array.each do |item|
|
18
|
+
command = yield item
|
19
|
+
name = name_for_item(item, id: id)
|
20
|
+
ply.register(name, command)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def register(name, command)
|
26
|
+
@list[name.to_s] = Command.new(name, command)
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
private_class_method def self.name_for_item(item, id:)
|
33
|
+
case id
|
34
|
+
when Symbol
|
35
|
+
item.send(id)
|
36
|
+
when Proc
|
37
|
+
id.call(item)
|
38
|
+
else
|
39
|
+
raise ArgumentError, "What is #{id.inspect}?!"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def run
|
44
|
+
if @sequential
|
45
|
+
run_sequentially
|
46
|
+
else
|
47
|
+
run_concurrently
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def run_concurrently
|
52
|
+
statuses = []
|
53
|
+
with_io_handler do |io_handler|
|
54
|
+
commands.each(&:run)
|
55
|
+
threads = commands.map do |command|
|
56
|
+
Thread.new do
|
57
|
+
pid_and_status = ::Process.wait2(command.pid)
|
58
|
+
io_handler.handle_exit_statuses!([pid_and_status], commands)
|
59
|
+
statuses << pid_and_status
|
60
|
+
end
|
61
|
+
end
|
62
|
+
threads.each(&:join)
|
63
|
+
end
|
64
|
+
verify_exit_statuses(statuses, commands)
|
65
|
+
end
|
66
|
+
|
67
|
+
def run_sequentially
|
68
|
+
with_io_handler do |io_handler|
|
69
|
+
commands.each do |command|
|
70
|
+
command.run
|
71
|
+
statuses = ::Process.waitall
|
72
|
+
io_handler.handle_exit_statuses!(statuses, commands)
|
73
|
+
verify_exit_statuses(statuses, commands)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def with_io_handler
|
79
|
+
io_handler = @io_handler_class.new(ios)
|
80
|
+
io_handler.start
|
81
|
+
thread = Thread.new { io_handler.handle_io! }
|
82
|
+
result = yield io_handler
|
83
|
+
thread.terminate
|
84
|
+
io_handler.stop
|
85
|
+
result
|
86
|
+
end
|
87
|
+
|
88
|
+
def ios
|
89
|
+
commands.flat_map(&:ios)
|
90
|
+
end
|
91
|
+
|
92
|
+
def commands
|
93
|
+
@list.values
|
94
|
+
end
|
95
|
+
|
96
|
+
def verify_exit_statuses(statuses, commands)
|
97
|
+
failure_reports = statuses
|
98
|
+
.map(&:last)
|
99
|
+
.reject(&:success?)
|
100
|
+
.map { |status| CommandReport.gather(status, commands) }
|
101
|
+
return if failure_reports.none?
|
102
|
+
|
103
|
+
warn ""
|
104
|
+
warn failure_reports.join("\n")
|
105
|
+
raise Failure, "Not every process was successful; #{failure_reports.size} failed"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "concurrent/array"
|
2
|
+
|
3
|
+
module Plywood
|
4
|
+
class FancyLogger < IOHandler
|
5
|
+
class FancyLog
|
6
|
+
attr_reader :exit_status, :name, :output
|
7
|
+
|
8
|
+
def initialize(name)
|
9
|
+
@name = name
|
10
|
+
@exit_status = nil
|
11
|
+
@output = Concurrent::Array.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_line(stream)
|
15
|
+
raise "Process exited" unless exit_status.nil?
|
16
|
+
stream.gets.each_line do |line|
|
17
|
+
@output << [(stream.err? ? :stderr : :stdout), line]
|
18
|
+
end
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def exit_status=(status)
|
23
|
+
@exit_status = status.to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def running?
|
27
|
+
@exit_status.nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def success?
|
31
|
+
@exit_status&.zero?
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
"#{icon} #{name} | #{status}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def status
|
39
|
+
case exit_status
|
40
|
+
when nil then "running"
|
41
|
+
when 0 then "success"
|
42
|
+
else "failed (#{exit_status})"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def icon
|
47
|
+
case exit_status
|
48
|
+
when nil then "⏵"
|
49
|
+
when 0 then "✔"
|
50
|
+
else "‼"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "io/console"
|
2
|
+
|
3
|
+
module Plywood
|
4
|
+
class FancyLogger < IOHandler
|
5
|
+
class Frame
|
6
|
+
def initialize(io, rows = nil, cols = nil)
|
7
|
+
rows, cols = IO.console.winsize if rows.nil? || cols.nil?
|
8
|
+
@io = io
|
9
|
+
@rows = rows.to_i - 1
|
10
|
+
@cols = cols.to_i
|
11
|
+
@lines = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.render(*, &)
|
15
|
+
frame = new(*)
|
16
|
+
yield frame
|
17
|
+
frame.render
|
18
|
+
end
|
19
|
+
|
20
|
+
def line(string, color = nil)
|
21
|
+
return if @lines.size >= @rows
|
22
|
+
string = string.to_s[0, @cols].ljust(@cols)
|
23
|
+
string = @io.color(color) + string + @io.color(:reset) if color
|
24
|
+
@lines << string
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def render
|
29
|
+
@lines << "".ljust(@cols) until @lines.size == @rows
|
30
|
+
@lines.each.with_index do |line, index|
|
31
|
+
# position_cursor(@rows - @lines.size + index + 1, 1)
|
32
|
+
position_cursor(index + 1, 1)
|
33
|
+
@io << line
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def position_cursor(row, col)
|
40
|
+
@io.write "#{CSI}#{row};#{col}H"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require_relative "color"
|
2
|
+
require_relative "io_handler"
|
3
|
+
require_relative "fancy_logger/fancy_log"
|
4
|
+
require_relative "fancy_logger/frame"
|
5
|
+
|
6
|
+
module Plywood
|
7
|
+
class FancyLogger < IOHandler
|
8
|
+
CSI = "\e[".freeze
|
9
|
+
FPS = 20 # frames per second
|
10
|
+
STOP_DELAY = 0.6 # seconds
|
11
|
+
|
12
|
+
def initialize(ios)
|
13
|
+
super
|
14
|
+
@names = ios.map(&:name).uniq
|
15
|
+
@logs = @names
|
16
|
+
.map { |name, _index| [name, FancyLog.new(name)] }
|
17
|
+
.to_h
|
18
|
+
Color.enable($stdout)
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
@initial_console_mode = $stdout.console_mode
|
23
|
+
IO.console.raw!
|
24
|
+
hide_cursor
|
25
|
+
use_alternate_screen_buffer
|
26
|
+
@render_thread = Thread.new do
|
27
|
+
loop do
|
28
|
+
render
|
29
|
+
sleep(1.0 / FPS)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop
|
36
|
+
sleep STOP_DELAY
|
37
|
+
Thread.kill(@render_thread) unless @render_thread.nil?
|
38
|
+
# IO.console.cooked!
|
39
|
+
$stdout.console_mode = @initial_console_mode
|
40
|
+
use_main_screen_buffer
|
41
|
+
show_cursor
|
42
|
+
render_report
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def handle_io!
|
47
|
+
loop do
|
48
|
+
readers, _writers = IO.select(@ios)
|
49
|
+
readers.each { |stream| copy_stream(stream) }
|
50
|
+
end
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def handle_exit_statuses!(statuses, commands)
|
55
|
+
statuses.each do |pid, status|
|
56
|
+
command = commands.detect { |item| item.pid == pid }
|
57
|
+
@logs[command.name].exit_status = status
|
58
|
+
end
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def copy_stream(stream)
|
65
|
+
if stream.eof?
|
66
|
+
@ios.delete(stream)
|
67
|
+
return self
|
68
|
+
end
|
69
|
+
@logs[stream.name].add_line(stream)
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
def render
|
74
|
+
Frame.render($stdout) do |frame|
|
75
|
+
@logs.each_value do |log|
|
76
|
+
frame.line(log, color(log.name))
|
77
|
+
next unless log.running?
|
78
|
+
gather_output(log).each do |line|
|
79
|
+
frame.line(line)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# show last x lines of output
|
86
|
+
def gather_output(log, amount = 8)
|
87
|
+
lines = log.output.last(amount).map(&:last)
|
88
|
+
lines << "" until lines.size == amount
|
89
|
+
lines
|
90
|
+
end
|
91
|
+
|
92
|
+
def render_report
|
93
|
+
render_success_report
|
94
|
+
render_failures_report
|
95
|
+
end
|
96
|
+
|
97
|
+
def render_success_report
|
98
|
+
@logs.each_value do |log|
|
99
|
+
next unless log.success?
|
100
|
+
header = $stdout.color color(log.name)
|
101
|
+
header << log.to_s
|
102
|
+
header << $stdout.color(:reset)
|
103
|
+
puts header
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def render_failures_report
|
108
|
+
@logs.each_value do |log|
|
109
|
+
next if log.success?
|
110
|
+
puts
|
111
|
+
$stdout.puts failure_log_header(log)
|
112
|
+
log.output.each do |line|
|
113
|
+
case line.first
|
114
|
+
when :stdout
|
115
|
+
$stdout.puts line.last
|
116
|
+
when :stderr
|
117
|
+
$stderr.puts line.last
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def failure_log_header(log)
|
124
|
+
$stdout.color(color(log.name)) << "-" * 40 << "\n" << log.to_s << $stdout.color(:reset)
|
125
|
+
end
|
126
|
+
|
127
|
+
COLORS = %w[cyan yellow green magenta red blue bright_cyan bright_yellow
|
128
|
+
bright_green bright_magenta bright_red bright_blue].freeze
|
129
|
+
|
130
|
+
def color(name)
|
131
|
+
COLORS[@names.index(name) % COLORS.length]
|
132
|
+
end
|
133
|
+
|
134
|
+
def use_alternate_screen_buffer
|
135
|
+
$stdout.write "#{CSI}?1049h"
|
136
|
+
end
|
137
|
+
|
138
|
+
def use_main_screen_buffer
|
139
|
+
$stdout.write "#{CSI}?1049l"
|
140
|
+
end
|
141
|
+
|
142
|
+
def hide_cursor
|
143
|
+
$stdout.write "#{CSI}?25l"
|
144
|
+
end
|
145
|
+
|
146
|
+
def show_cursor
|
147
|
+
$stdout.write "#{CSI}?25h"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative "color"
|
2
|
+
require_relative "io_handler"
|
3
|
+
|
4
|
+
module Plywood
|
5
|
+
# An IOHandler that outputs all process output, line by line, prefixed with "name | ".
|
6
|
+
#
|
7
|
+
# It keeps stderr and stdout separated which may be convenient if you want to pipe
|
8
|
+
# the output to another program.
|
9
|
+
class Logger < IOHandler
|
10
|
+
def initialize(ios)
|
11
|
+
super
|
12
|
+
@name_padding = [6, ios.map(&:name).map(&:size).max].max
|
13
|
+
@names = ios.map(&:name).uniq
|
14
|
+
Color.enable($stderr)
|
15
|
+
Color.enable($stdout)
|
16
|
+
end
|
17
|
+
|
18
|
+
def handle_io!
|
19
|
+
loop do
|
20
|
+
readers, _writers = IO.select(@ios)
|
21
|
+
readers.each { |stream| copy_stream_to_io(stream) }
|
22
|
+
end
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def handle_exit_statuses!(statuses, commands)
|
27
|
+
statuses.each do |pid, status|
|
28
|
+
next if status.success?
|
29
|
+
command = commands.detect { |item| item.pid == pid }
|
30
|
+
log($stderr, command.name, "`#{command}` failed with status #{status.to_i}")
|
31
|
+
end
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def copy_stream_to_io(stream)
|
38
|
+
if stream.eof?
|
39
|
+
@ios.delete(stream)
|
40
|
+
return self
|
41
|
+
end
|
42
|
+
|
43
|
+
io = stream.err? ? $stderr : $stdout
|
44
|
+
log(io, stream.name, stream.gets)
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def log(io, name, string)
|
49
|
+
output = io.color color(name)
|
50
|
+
output << name.ljust(@name_padding, " ") << " | "
|
51
|
+
output << io.color(:reset)
|
52
|
+
output << string
|
53
|
+
io.puts output
|
54
|
+
io.flush
|
55
|
+
end
|
56
|
+
|
57
|
+
def name_process_and_io(stream)
|
58
|
+
@processes.each do |name, process|
|
59
|
+
return [name, process, $stderr] if process.stderr == stream
|
60
|
+
return [name, process, $stdout] if process.stdout == stream
|
61
|
+
end
|
62
|
+
raise IndexError
|
63
|
+
end
|
64
|
+
|
65
|
+
def name_and_process(pid)
|
66
|
+
@processes.each do |name, process|
|
67
|
+
return [name, process] if process.pid == pid
|
68
|
+
end
|
69
|
+
raise IndexError
|
70
|
+
end
|
71
|
+
|
72
|
+
COLORS = %w[cyan yellow green magenta red blue bright_cyan bright_yellow
|
73
|
+
bright_green bright_magenta bright_red bright_blue].freeze
|
74
|
+
|
75
|
+
def color(name)
|
76
|
+
COLORS[@names.index(name) % COLORS.length]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "delegate"
|
2
|
+
|
3
|
+
module Plywood
|
4
|
+
# This class is used as a wrapper around an io.
|
5
|
+
# It adds extra methods that are useful to distinguish it from other IOs in an efficient way.
|
6
|
+
class NamedIO < SimpleDelegator
|
7
|
+
attr_reader :name, :stream
|
8
|
+
|
9
|
+
def initialize(io, name, stream)
|
10
|
+
super(io)
|
11
|
+
@name = name.to_s
|
12
|
+
@stream = stream.to_sym
|
13
|
+
validate_stream
|
14
|
+
end
|
15
|
+
|
16
|
+
def err?
|
17
|
+
@stream == :err
|
18
|
+
end
|
19
|
+
|
20
|
+
def out?
|
21
|
+
@stream == :out
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def validate_stream
|
27
|
+
return if [:err, :out].include? @stream
|
28
|
+
raise ArgumentError, "stream cannot be #{@stream}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/plywood.rb
ADDED
data/logo.jpeg
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: plywood
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tijn Schuurmans
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-04-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest-focus
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop-minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sdoc
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: simplecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: spy
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Run multiple commands in parallel; consolidate the output
|
126
|
+
email: plywood@tijnschuurmans.nl
|
127
|
+
executables: []
|
128
|
+
extensions: []
|
129
|
+
extra_rdoc_files:
|
130
|
+
- README.md
|
131
|
+
- logo.jpeg
|
132
|
+
files:
|
133
|
+
- README.md
|
134
|
+
- lib/plywood.rb
|
135
|
+
- lib/plywood/color.rb
|
136
|
+
- lib/plywood/command.rb
|
137
|
+
- lib/plywood/command_report.rb
|
138
|
+
- lib/plywood/commands.rb
|
139
|
+
- lib/plywood/fancy_logger.rb
|
140
|
+
- lib/plywood/fancy_logger/fancy_log.rb
|
141
|
+
- lib/plywood/fancy_logger/frame.rb
|
142
|
+
- lib/plywood/io_handler.rb
|
143
|
+
- lib/plywood/logger.rb
|
144
|
+
- lib/plywood/named_io.rb
|
145
|
+
- lib/plywood/version.rb
|
146
|
+
- logo.jpeg
|
147
|
+
homepage: https://github.com/Jobport/plywood
|
148
|
+
licenses:
|
149
|
+
- Copyright 2023 Jobport B.V.
|
150
|
+
- MIT
|
151
|
+
metadata:
|
152
|
+
homepage_uri: https://github.com/Jobport/plywood
|
153
|
+
changelog_uri: https://github.com/Jobport/plywood/releases
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: 3.3.0
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0'
|
168
|
+
requirements: []
|
169
|
+
rubygems_version: 3.5.22
|
170
|
+
signing_key:
|
171
|
+
specification_version: 4
|
172
|
+
summary: The simplest multiplexer
|
173
|
+
test_files: []
|