dry-stack 0.1.54 → 0.1.56

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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/bin/rparallel +130 -0
  3. data/lib/version.rb +1 -1
  4. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed1023e354efbaab3d973e49eddfea5263cecaa67f923b6033c9c29bc5a72b53
4
- data.tar.gz: 7736759f1f835f8943386f1a3f36493c54d2f36b99d2b11c6404e8488bf2668a
3
+ metadata.gz: f703dca5d6c72d78f7634b294ba41b739f19b5ee87c0177301ce0d8fc98e7148
4
+ data.tar.gz: 7f70554b5b5b421926ce36dc75e1fec8086d98a2df2f51c9b8763441ad869057
5
5
  SHA512:
6
- metadata.gz: e289b884edf0fdafc68fc5e13aeffec712f5c7309937f692bf328f07d8bc9eed593de91619b87baf5289a9148f02c7aa87370fb596334b524ae110e21d9a6af1
7
- data.tar.gz: 13e7747378d96bd699361df4610e05f386a1a7503abece52b2233ba451236bb3b383db1e0e5267880a7d1c067f2a782181102c959546cb99c3d76c3c6c9eebef
6
+ metadata.gz: f8f8588139024f0a15c9ac97b103a545a5d0d997d7e6af7b4db07f9574bc242c25c9a2f4a3ff8caf0c6a4b01a4d7e70910a7eb59f6a561edc735ed9297850fd6
7
+ data.tar.gz: 37e9151d6f369f35eda3286c418758292c515526106c3c3e04b27c5e391ebc5908482675a0b9a80fe20db7bd6968f5e4e5566bf2b27b1304357f218924def989
data/bin/rparallel ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "open3"
5
+
6
+ Task = Struct.new(:number, :command, :started_at, :ended_at, :exit_code, keyword_init: true) do
7
+ def duration = ended_at - started_at
8
+
9
+ def status = exit_code.zero? ? "ok" : "failed"
10
+ end
11
+
12
+ class RParallel
13
+ COLORS = {
14
+ bold: "\e[1m",
15
+ dim: "\e[2m",
16
+ green: "\e[32m",
17
+ red: "\e[31m",
18
+ cyan: "\e[36m",
19
+ reset: "\e[0m"
20
+ }.freeze
21
+
22
+ def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr)
23
+ return usage(stdout) if argv == ["--help"] || argv == ["-h"]
24
+
25
+ unless argv.empty?
26
+ stderr.puts "Unsupported arguments: #{argv.join(' ')}"
27
+ return 2
28
+ end
29
+
30
+ commands = stdin.each_line.map(&:strip).reject(&:empty?)
31
+ tasks = commands.each_with_index.map { |command, index| Task.new(number: index + 1, command:) }
32
+
33
+ color = color_enabled?
34
+ output_mutex = Mutex.new
35
+ tasks.map { |task| Thread.new { run_task(task, stdout, stderr, output_mutex, color) } }.each(&:join)
36
+ print_report(tasks, stdout)
37
+
38
+ tasks.all? { _1.exit_code.zero? } ? 0 : 1
39
+ end
40
+
41
+ def self.run_task(task, stdout, stderr, output_mutex, color)
42
+ task.started_at = monotonic_time
43
+
44
+ Open3.popen3("sh", "-c", task.command) do |child_stdin, child_stdout, child_stderr, wait_thread|
45
+ child_stdin.close
46
+ readers = [
47
+ Thread.new { stream_output(child_stdout, stdout, task.number, output_mutex) },
48
+ Thread.new { stream_output(child_stderr, stderr, task.number, output_mutex, color: :red, enabled: color) }
49
+ ]
50
+ readers.each(&:join)
51
+ task.exit_code = wait_thread.value.exitstatus || 1
52
+ end
53
+ rescue StandardError => e
54
+ stderr.puts "Task #{task.number} failed to start: #{e.message}"
55
+ task.exit_code = 1
56
+ ensure
57
+ task.ended_at = monotonic_time
58
+ end
59
+
60
+ def self.stream_output(source, target, job_number, output_mutex, color: nil, enabled: false)
61
+ source.each_line do |line|
62
+ output = "[Job #{job_number}] #{line.chomp}"
63
+ output_mutex.synchronize { target.puts color ? colorize(output, color, enabled) : output }
64
+ end
65
+ end
66
+
67
+ def self.print_report(tasks, stdout)
68
+ rows = tasks.map do |task|
69
+ [task.number.to_s, task.status, task.exit_code.to_s, format("%.3fs", task.duration), task.command]
70
+ end
71
+ header = ["#", "status", "exit", "duration", "command"]
72
+ widths = column_widths([header, *rows])
73
+ color = color_enabled?
74
+
75
+ stdout.puts
76
+ stdout.puts colorize("rparallel report", :bold, color)
77
+ stdout.puts colorize(border(widths), :dim, color)
78
+ stdout.puts format_row(header, widths, color:, header: true)
79
+ stdout.puts colorize(border(widths), :dim, color)
80
+ rows.each { stdout.puts format_row(_1, widths, color:) }
81
+ stdout.puts colorize(border(widths), :dim, color)
82
+ end
83
+
84
+ def self.column_widths(rows)
85
+ rows.transpose.map { _1.map(&:length).max }
86
+ end
87
+
88
+ def self.format_row(row, widths, color:, header: false)
89
+ cells = row.each_with_index.map do |value, index|
90
+ padded = value.ljust(widths[index])
91
+ header ? colorize(padded, :cyan, color) : colorize_report_cell(padded, row, index, color)
92
+ end
93
+
94
+ "| #{cells.join(' | ')} |"
95
+ end
96
+
97
+ def self.border(widths)
98
+ "+-#{widths.map { '-' * _1 }.join('-+-')}-+"
99
+ end
100
+
101
+ def self.colorize_report_cell(value, row, index, color)
102
+ return colorize(value, row[1] == "ok" ? :green : :red, color) if [1, 2].include?(index)
103
+
104
+ value
105
+ end
106
+
107
+ def self.colorize(value, color_name, enabled)
108
+ return value unless enabled
109
+
110
+ "#{COLORS.fetch(color_name)}#{value}#{COLORS.fetch(:reset)}"
111
+ end
112
+
113
+ def self.color_enabled?
114
+ ENV["NO_COLOR"].nil? && ENV["RPARALLEL_NO_COLOR"].nil?
115
+ end
116
+
117
+ def self.usage(stdout)
118
+ stdout.puts "Usage: rparallel < tasks.txt"
119
+ stdout.puts
120
+ stdout.puts "Reads non-empty stdin lines as shell tasks, runs them in parallel, then prints a report."
121
+ 0
122
+ end
123
+
124
+ def self.monotonic_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
125
+
126
+ private_class_method :run_task, :stream_output, :print_report, :column_widths, :format_row, :border,
127
+ :colorize_report_cell, :colorize, :color_enabled?, :usage, :monotonic_time
128
+ end
129
+
130
+ exit RParallel.run(ARGV)
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  class Stack
3
- VERSION = '0.1.54'
3
+ VERSION = '0.1.56'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-stack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.54
4
+ version: 0.1.56
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artyom B
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-24 00:00:00.000000000 Z
11
+ date: 2026-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -98,10 +98,12 @@ description: ''
98
98
  email: author@email.address
99
99
  executables:
100
100
  - dry-stack
101
+ - rparallel
101
102
  extensions: []
102
103
  extra_rdoc_files: []
103
104
  files:
104
105
  - bin/dry-stack
106
+ - bin/rparallel
105
107
  - lib/dry-stack.rb
106
108
  - lib/dry-stack/apache_specific_md5.rb
107
109
  - lib/dry-stack/command_compose.rb