ruby-progress 1.3.2 → 1.3.5
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/.rubocop_todo.yml +37 -18
- data/CHANGELOG.md +64 -118
- data/DAEMON_MODE.md +127 -0
- data/Gemfile +9 -1
- data/Gemfile.lock +25 -18
- data/JOB_CLI_REFACTOR.md +67 -0
- data/README.md +90 -94
- data/bin/prg +10 -16
- data/demo_screencast.rb +130 -122
- data/examples/daemon_job_example.sh +8 -10
- data/lib/ruby-progress/cli/fill_options.rb +8 -6
- data/lib/ruby-progress/cli/job_cli.rb +170 -131
- data/lib/ruby-progress/cli/ripple_cli.rb +19 -56
- data/lib/ruby-progress/cli/ripple_options.rb +18 -3
- data/lib/ruby-progress/cli/twirl_cli.rb +3 -1
- data/lib/ruby-progress/cli/twirl_options.rb +4 -4
- data/lib/ruby-progress/cli/twirl_runner.rb +3 -37
- data/lib/ruby-progress/cli/worm_cli.rb +2 -50
- data/lib/ruby-progress/cli/worm_options.rb +4 -6
- data/lib/ruby-progress/cli/worm_runner.rb +16 -5
- data/lib/ruby-progress/daemon.rb +2 -64
- data/lib/ruby-progress/fill_cli.rb +4 -72
- data/lib/ruby-progress/output_capture.rb +174 -37
- data/lib/ruby-progress/utils.rb +11 -6
- data/lib/ruby-progress/version.rb +5 -5
- data/lib/ruby-progress/worm.rb +8 -61
- data/ruby-progress.gemspec +41 -0
- data/screencast +2497 -26
- data/screencast.svg +1 -0
- data/scripts/coverage_analysis.rb +49 -0
- data/test_daemon.sh +20 -0
- metadata +31 -11
|
@@ -1,159 +1,198 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# CLI: prg job
|
|
4
|
-
|
|
5
|
-
# Provides the `prg job send` helper to enqueue commands into a daemon's
|
|
6
|
-
# job directory. This file contains a minimal implementation used by tests.
|
|
3
|
+
# CLI: prg job [subcommand]
|
|
4
|
+
# Send control messages to backgrounded progress indicators.
|
|
7
5
|
|
|
8
6
|
require 'optparse'
|
|
9
7
|
require 'json'
|
|
10
|
-
require 'securerandom'
|
|
11
8
|
require 'fileutils'
|
|
12
9
|
require_relative '../daemon'
|
|
13
10
|
|
|
14
|
-
#
|
|
11
|
+
# JobCLI - sends control messages to backgrounded progress indicators
|
|
15
12
|
#
|
|
16
|
-
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# prg job stop --daemon-name mytask [--message "Done!"] [--checkmark]
|
|
15
|
+
# prg job advance --daemon-name mytask [--amount 10]
|
|
16
|
+
# prg job status --daemon-name mytask
|
|
17
17
|
module JobCLI
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# the daemon's job processor.
|
|
24
|
-
# Simple CLI for submitting jobs to a running daemon job directory.
|
|
25
|
-
# Usage: prg job send --pid-file /tmp/... --command "echo hi" [--wait]
|
|
26
|
-
class Options
|
|
27
|
-
def self.parse(argv)
|
|
28
|
-
options = { wait: false }
|
|
29
|
-
opt = OptionParser.new do |o|
|
|
30
|
-
o.banner = 'Usage: prg job send [options]'
|
|
31
|
-
o.on('--pid-file PATH', 'Path to daemon pid file') do |v|
|
|
32
|
-
options[:pid_file] = v
|
|
33
|
-
end
|
|
34
|
-
o.on('--daemon-name NAME', 'Daemon name (maps to /tmp/ruby-progress/NAME.pid)') do |v|
|
|
35
|
-
options[:daemon_name] = v
|
|
36
|
-
end
|
|
37
|
-
o.on('--command CMD', 'Command to run') do |v|
|
|
38
|
-
options[:command] = v
|
|
39
|
-
end
|
|
40
|
-
o.on('--stdin', 'Read command from stdin (overrides --command)') do
|
|
41
|
-
options[:stdin] = true
|
|
42
|
-
end
|
|
43
|
-
o.on('--advance', 'Send an advance action (no value)') do
|
|
44
|
-
options[:action] = 'advance'
|
|
45
|
-
end
|
|
46
|
-
o.on('--percent N', Integer, 'Send a percent action with value N') do |v|
|
|
47
|
-
options[:action] = 'percent'
|
|
48
|
-
options[:value] = v
|
|
49
|
-
end
|
|
50
|
-
o.on('--complete', 'Send a complete action (no value)') do
|
|
51
|
-
options[:action] = 'complete'
|
|
52
|
-
end
|
|
53
|
-
o.on('--cancel', 'Send a cancel action (no value)') do
|
|
54
|
-
options[:action] = 'cancel'
|
|
55
|
-
end
|
|
56
|
-
o.on('--action ACTION', 'Send a custom action name') do |v|
|
|
57
|
-
options[:action] = v
|
|
58
|
-
end
|
|
59
|
-
o.on('--value VAL', 'Value for the action (string or number)') do |v|
|
|
60
|
-
options[:value] = v
|
|
61
|
-
end
|
|
62
|
-
o.on('--wait', 'Wait for result file and print it') do
|
|
63
|
-
options[:wait] = true
|
|
64
|
-
end
|
|
65
|
-
o.on('--timeout SECONDS', Integer, 'Timeout seconds for wait') do |v|
|
|
66
|
-
options[:timeout] = v
|
|
67
|
-
end
|
|
68
|
-
end
|
|
18
|
+
def self.run(argv = ARGV)
|
|
19
|
+
if argv.empty?
|
|
20
|
+
print_help
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
69
23
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
24
|
+
subcommand = argv.shift
|
|
25
|
+
|
|
26
|
+
case subcommand
|
|
27
|
+
when 'stop'
|
|
28
|
+
stop(argv)
|
|
29
|
+
when 'advance'
|
|
30
|
+
advance(argv)
|
|
31
|
+
when 'status'
|
|
32
|
+
status(argv)
|
|
33
|
+
when 'send'
|
|
34
|
+
# Backward compatibility: 'send' is now 'stop'
|
|
35
|
+
warn "Warning: 'prg job send' is deprecated. Use 'prg job stop' instead."
|
|
36
|
+
stop(argv)
|
|
37
|
+
when '--help', '-h'
|
|
38
|
+
print_help
|
|
39
|
+
else
|
|
40
|
+
warn "Error: Unknown subcommand '#{subcommand}'"
|
|
41
|
+
print_help
|
|
42
|
+
exit 1
|
|
73
43
|
end
|
|
74
44
|
end
|
|
75
45
|
|
|
76
|
-
def self.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if is_action
|
|
100
|
-
if cmd && !cmd.strip.empty?
|
|
101
|
-
warn 'Cannot specify both --command/--stdin and an action flag'
|
|
102
|
-
exit 1
|
|
103
|
-
end
|
|
46
|
+
def self.print_help
|
|
47
|
+
puts 'Usage: prg job [subcommand] [options]'
|
|
48
|
+
puts
|
|
49
|
+
puts 'Subcommands:'
|
|
50
|
+
puts ' stop Stop a running progress indicator'
|
|
51
|
+
puts ' advance Advance a fill progress bar'
|
|
52
|
+
puts ' status Check status of a running indicator'
|
|
53
|
+
puts
|
|
54
|
+
puts 'Common Options:'
|
|
55
|
+
puts ' --daemon-name NAME Name of the daemon to control'
|
|
56
|
+
puts ' --pid-file PATH Path to daemon PID file'
|
|
57
|
+
puts
|
|
58
|
+
puts 'Examples:'
|
|
59
|
+
puts ' prg job stop --daemon-name mytask --message "Complete!"'
|
|
60
|
+
puts ' prg job advance --daemon-name mybar --amount 10'
|
|
61
|
+
puts ' prg job status --daemon-name mytask'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.resolve_pid_file(opts)
|
|
65
|
+
if opts[:pid_file]
|
|
66
|
+
opts[:pid_file]
|
|
67
|
+
elsif opts[:daemon_name]
|
|
68
|
+
"/tmp/ruby-progress/#{opts[:daemon_name]}.pid"
|
|
104
69
|
else
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
70
|
+
RubyProgress::Daemon.default_pid_file
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.stop(argv)
|
|
75
|
+
opts = parse_stop_options(argv)
|
|
76
|
+
pid_file = resolve_pid_file(opts)
|
|
77
|
+
|
|
78
|
+
unless File.exist?(pid_file)
|
|
79
|
+
warn "PID file #{pid_file} not found. Is the daemon running?"
|
|
80
|
+
exit 1
|
|
109
81
|
end
|
|
110
82
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
83
|
+
RubyProgress::Daemon.stop_daemon_by_pid_file(
|
|
84
|
+
pid_file,
|
|
85
|
+
message: opts[:message],
|
|
86
|
+
checkmark: opts[:checkmark] || false,
|
|
87
|
+
error: opts[:error] || false
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Don't output confirmation - the daemon itself shows the completion message
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.advance(argv)
|
|
94
|
+
opts = parse_advance_options(argv)
|
|
95
|
+
pid_file = resolve_pid_file(opts)
|
|
96
|
+
|
|
97
|
+
unless File.exist?(pid_file)
|
|
98
|
+
warn "PID file #{pid_file} not found. Is the daemon running?"
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Write advance command to control message file
|
|
103
|
+
cmf = RubyProgress::Daemon.control_message_file(pid_file)
|
|
104
|
+
control_data = {
|
|
105
|
+
action: 'advance',
|
|
106
|
+
amount: opts[:amount] || 1,
|
|
107
|
+
total: opts[:total]
|
|
108
|
+
}.compact
|
|
109
|
+
|
|
110
|
+
File.write(cmf, JSON.generate(control_data))
|
|
111
|
+
|
|
112
|
+
# Send signal to daemon to check for messages
|
|
113
|
+
pid = File.read(pid_file).strip.to_i
|
|
114
|
+
begin
|
|
115
|
+
Process.kill('USR2', pid)
|
|
116
|
+
rescue Errno::ESRCH
|
|
117
|
+
# Process doesn't exist
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Silent operation for script-friendly usage
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.status(argv)
|
|
124
|
+
opts = parse_status_options(argv)
|
|
125
|
+
pid_file = resolve_pid_file(opts)
|
|
126
|
+
|
|
127
|
+
RubyProgress::Daemon.show_status(pid_file)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.parse_stop_options(argv)
|
|
131
|
+
options = {}
|
|
132
|
+
opt = OptionParser.new do |o|
|
|
133
|
+
o.banner = 'Usage: prg job stop [options]'
|
|
134
|
+
o.on('--pid-file PATH', 'Path to daemon PID file') do |v|
|
|
135
|
+
options[:pid_file] = v
|
|
136
|
+
end
|
|
137
|
+
o.on('--daemon-name NAME', 'Daemon name (maps to /tmp/ruby-progress/NAME.pid)') do |v|
|
|
138
|
+
options[:daemon_name] = v
|
|
139
|
+
end
|
|
140
|
+
o.on('--message MSG', 'Optional completion message to display') do |v|
|
|
141
|
+
options[:message] = v
|
|
142
|
+
end
|
|
143
|
+
o.on('--checkmark', 'Display a checkmark on completion') do
|
|
144
|
+
options[:checkmark] = true
|
|
145
|
+
end
|
|
146
|
+
o.on('--error', 'Mark completion as error state') do
|
|
147
|
+
options[:error] = true
|
|
134
148
|
end
|
|
135
|
-
else
|
|
136
|
-
puts job_id
|
|
137
149
|
end
|
|
150
|
+
|
|
151
|
+
opt.parse(argv)
|
|
152
|
+
options
|
|
138
153
|
end
|
|
139
154
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
def self.parse_advance_options(argv)
|
|
156
|
+
options = {}
|
|
157
|
+
opt = OptionParser.new do |o|
|
|
158
|
+
o.banner = 'Usage: prg job advance [options]'
|
|
159
|
+
o.on('--pid-file PATH', 'Path to daemon PID file') do |v|
|
|
160
|
+
options[:pid_file] = v
|
|
161
|
+
end
|
|
162
|
+
o.on('--daemon-name NAME', 'Daemon name (maps to /tmp/ruby-progress/NAME.pid)') do |v|
|
|
163
|
+
options[:daemon_name] = v
|
|
164
|
+
end
|
|
165
|
+
o.on('--amount N', Integer, 'Amount to advance (default: 1)') do |v|
|
|
166
|
+
options[:amount] = v
|
|
167
|
+
end
|
|
168
|
+
o.on('--total N', Integer, 'Update total if needed') do |v|
|
|
169
|
+
options[:total] = v
|
|
170
|
+
end
|
|
171
|
+
end
|
|
143
172
|
|
|
144
|
-
|
|
173
|
+
opt.parse(argv)
|
|
174
|
+
options
|
|
175
|
+
end
|
|
145
176
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
177
|
+
def self.parse_status_options(argv)
|
|
178
|
+
options = {}
|
|
179
|
+
opt = OptionParser.new do |o|
|
|
180
|
+
o.banner = 'Usage: prg job status [options]'
|
|
181
|
+
o.on('--pid-file PATH', 'Path to daemon PID file') do |v|
|
|
182
|
+
options[:pid_file] = v
|
|
183
|
+
end
|
|
184
|
+
o.on('--daemon-name NAME', 'Daemon name (maps to /tmp/ruby-progress/NAME.pid)') do |v|
|
|
185
|
+
options[:daemon_name] = v
|
|
152
186
|
end
|
|
153
|
-
else
|
|
154
|
-
payload['command'] = cmd
|
|
155
187
|
end
|
|
156
188
|
|
|
157
|
-
|
|
189
|
+
opt.parse(argv)
|
|
190
|
+
options
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Backward compatibility
|
|
194
|
+
def self.send(argv = ARGV)
|
|
195
|
+
warn "Warning: 'JobCLI.send' is deprecated. Use 'JobCLI.stop' instead."
|
|
196
|
+
stop(argv)
|
|
158
197
|
end
|
|
159
198
|
end
|
|
@@ -8,6 +8,14 @@ require_relative '../output_capture'
|
|
|
8
8
|
|
|
9
9
|
# Enhanced Ripple CLI with unified flags (extracted from bin/prg)
|
|
10
10
|
module RippleCLI
|
|
11
|
+
def self.resolve_pid_file(options, name_key = :daemon_name)
|
|
12
|
+
return options[:pid_file] if options[:pid_file]
|
|
13
|
+
|
|
14
|
+
return "/tmp/ruby-progress/#{options[name_key]}.pid" if options[name_key]
|
|
15
|
+
|
|
16
|
+
RubyProgress::Daemon.default_pid_file
|
|
17
|
+
end
|
|
18
|
+
|
|
11
19
|
def self.run
|
|
12
20
|
trap('INT') do
|
|
13
21
|
RubyProgress::Utils.show_cursor
|
|
@@ -18,11 +26,11 @@ module RippleCLI
|
|
|
18
26
|
|
|
19
27
|
# Daemon/status/stop handling (process these without requiring text)
|
|
20
28
|
if options[:status]
|
|
21
|
-
pid_file = options
|
|
29
|
+
pid_file = resolve_pid_file(options, :status_name)
|
|
22
30
|
RubyProgress::Daemon.show_status(pid_file)
|
|
23
31
|
exit
|
|
24
32
|
elsif options[:stop]
|
|
25
|
-
pid_file = options
|
|
33
|
+
pid_file = resolve_pid_file(options, :stop_name)
|
|
26
34
|
stop_msg = options[:stop_error] || options[:stop_success]
|
|
27
35
|
is_error = !options[:stop_error].nil?
|
|
28
36
|
RubyProgress::Daemon.stop_daemon_by_pid_file(
|
|
@@ -33,13 +41,8 @@ module RippleCLI
|
|
|
33
41
|
)
|
|
34
42
|
exit
|
|
35
43
|
elsif options[:daemon]
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
if options[:no_detach]
|
|
39
|
-
PrgCLI.backgroundize
|
|
40
|
-
else
|
|
41
|
-
PrgCLI.daemonize
|
|
42
|
-
end
|
|
44
|
+
# Background without detaching so ripple remains visible in current terminal
|
|
45
|
+
PrgCLI.backgroundize
|
|
43
46
|
|
|
44
47
|
# For daemon mode, default message if none provided
|
|
45
48
|
text = options[:message] || ARGV.join(' ')
|
|
@@ -71,7 +74,12 @@ module RippleCLI
|
|
|
71
74
|
if $stdout.tty?
|
|
72
75
|
# Interactive TTY: use PTY-based capture so the animation can run while the
|
|
73
76
|
# command executes. We only print captured stdout if options[:output] == :stdout.
|
|
74
|
-
oc = RubyProgress::OutputCapture.new(
|
|
77
|
+
oc = RubyProgress::OutputCapture.new(
|
|
78
|
+
command: options[:command],
|
|
79
|
+
lines: options[:output_lines] || 3,
|
|
80
|
+
position: options[:output_position] || :above,
|
|
81
|
+
stream: options[:output] == :stdout || options[:stdout_live]
|
|
82
|
+
)
|
|
75
83
|
oc.start
|
|
76
84
|
|
|
77
85
|
# Create rippler. Attach output capture only when the user requested
|
|
@@ -120,7 +128,7 @@ module RippleCLI
|
|
|
120
128
|
end
|
|
121
129
|
|
|
122
130
|
def self.run_daemon_mode(text, options)
|
|
123
|
-
pid_file = options
|
|
131
|
+
pid_file = resolve_pid_file(options, :daemon_name)
|
|
124
132
|
FileUtils.mkdir_p(File.dirname(pid_file))
|
|
125
133
|
File.write(pid_file, Process.pid.to_s)
|
|
126
134
|
begin
|
|
@@ -134,9 +142,6 @@ module RippleCLI
|
|
|
134
142
|
Signal.trap('TERM') { stop_requested = true }
|
|
135
143
|
Signal.trap('HUP') { stop_requested = true }
|
|
136
144
|
|
|
137
|
-
job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
|
|
138
|
-
job_thread = Thread.new { process_daemon_jobs_for_rippler(job_dir, rippler, options) }
|
|
139
|
-
|
|
140
145
|
rippler.advance until stop_requested
|
|
141
146
|
ensure
|
|
142
147
|
RubyProgress::Utils.clear_line
|
|
@@ -179,51 +184,9 @@ module RippleCLI
|
|
|
179
184
|
end
|
|
180
185
|
|
|
181
186
|
# stop job thread and cleanup
|
|
182
|
-
job_thread&.kill
|
|
183
187
|
FileUtils.rm_f(pid_file)
|
|
184
188
|
end
|
|
185
189
|
end
|
|
186
190
|
|
|
187
|
-
def self.process_daemon_jobs_for_rippler(job_dir, rippler, options)
|
|
188
|
-
RubyProgress::Daemon.process_jobs(job_dir) do |job|
|
|
189
|
-
jid = job['id'] || SecureRandom.uuid
|
|
190
|
-
log_path = begin
|
|
191
|
-
File.join(File.dirname(job_dir), "#{jid}.log")
|
|
192
|
-
rescue StandardError
|
|
193
|
-
nil
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
oc = RubyProgress::OutputCapture.new(
|
|
197
|
-
command: job['command'],
|
|
198
|
-
lines: options[:output_lines] || 3,
|
|
199
|
-
position: options[:output_position] || :above,
|
|
200
|
-
log_path: log_path
|
|
201
|
-
)
|
|
202
|
-
oc.start
|
|
203
|
-
|
|
204
|
-
rippler.instance_variable_set(:@output_capture, oc)
|
|
205
|
-
oc.wait
|
|
206
|
-
captured = oc.lines.join("\n")
|
|
207
|
-
exit_status = oc.exit_status
|
|
208
|
-
rippler.instance_variable_set(:@output_capture, nil)
|
|
209
|
-
|
|
210
|
-
success = exit_status.to_i.zero?
|
|
211
|
-
if job['message']
|
|
212
|
-
RubyProgress::Utils.display_completion(
|
|
213
|
-
job['message'],
|
|
214
|
-
success: success,
|
|
215
|
-
show_checkmark: job['checkmark'] || false,
|
|
216
|
-
output_stream: :stdout,
|
|
217
|
-
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
218
|
-
)
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
{ 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
|
|
222
|
-
rescue StandardError
|
|
223
|
-
# ignore per-job errors; process_jobs will write result
|
|
224
|
-
nil
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
191
|
# Options parsing moved to ripple_options.rb
|
|
229
192
|
end
|
|
@@ -91,6 +91,10 @@ module RippleCLI
|
|
|
91
91
|
options[:output] = :stdout
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
opts.on('--stdout-live', 'Stream captured output to STDOUT as it arrives (non-blocking)') do
|
|
95
|
+
options[:stdout_live] = true
|
|
96
|
+
end
|
|
97
|
+
|
|
94
98
|
opts.on('--quiet', 'Suppress all output') do
|
|
95
99
|
options[:output] = :quiet
|
|
96
100
|
end
|
|
@@ -102,8 +106,9 @@ module RippleCLI
|
|
|
102
106
|
options[:daemon] = true
|
|
103
107
|
end
|
|
104
108
|
|
|
105
|
-
opts.on('--
|
|
106
|
-
options[:
|
|
109
|
+
opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
|
|
110
|
+
options[:daemon] = true
|
|
111
|
+
options[:daemon_name] = name
|
|
107
112
|
end
|
|
108
113
|
|
|
109
114
|
opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
|
|
@@ -114,10 +119,20 @@ module RippleCLI
|
|
|
114
119
|
options[:stop] = true
|
|
115
120
|
end
|
|
116
121
|
|
|
122
|
+
opts.on('--stop-id NAME', 'Stop daemon by name (automatically implies --stop)') do |name|
|
|
123
|
+
options[:stop] = true
|
|
124
|
+
options[:stop_name] = name
|
|
125
|
+
end
|
|
126
|
+
|
|
117
127
|
opts.on('--status', 'Show daemon status (running/not running)') do
|
|
118
128
|
options[:status] = true
|
|
119
129
|
end
|
|
120
130
|
|
|
131
|
+
opts.on('--status-id NAME', 'Show daemon status by name') do |name|
|
|
132
|
+
options[:status] = true
|
|
133
|
+
options[:status_name] = name
|
|
134
|
+
end
|
|
135
|
+
|
|
121
136
|
opts.on('--stop-success MESSAGE', 'When stopping, show this success message') do |msg|
|
|
122
137
|
options[:stop_success] = msg
|
|
123
138
|
end
|
|
@@ -131,7 +146,7 @@ module RippleCLI
|
|
|
131
146
|
opts.separator ''
|
|
132
147
|
opts.separator 'Daemon notes:'
|
|
133
148
|
opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
|
|
134
|
-
opts.separator ' - Use --
|
|
149
|
+
opts.separator ' - Use --daemon-as NAME for named daemons, or --stop-id/--status-id for named control.'
|
|
135
150
|
|
|
136
151
|
opts.separator ''
|
|
137
152
|
opts.separator 'General:'
|
|
@@ -26,7 +26,9 @@ module TwirlCLI
|
|
|
26
26
|
)
|
|
27
27
|
exit
|
|
28
28
|
elsif options[:daemon]
|
|
29
|
-
|
|
29
|
+
# Background without detaching so spinner remains visible in current terminal
|
|
30
|
+
PrgCLI.backgroundize
|
|
31
|
+
|
|
30
32
|
TwirlRunner.run_daemon_mode(options)
|
|
31
33
|
elsif options[:command]
|
|
32
34
|
TwirlRunner.run_with_command(options)
|
|
@@ -81,6 +81,10 @@ module TwirlCLI
|
|
|
81
81
|
options[:stdout] = true
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
opts.on('--stdout-live', 'Stream captured output to STDOUT as it arrives (non-blocking)') do
|
|
85
|
+
options[:stdout_live] = true
|
|
86
|
+
end
|
|
87
|
+
|
|
84
88
|
opts.separator ''
|
|
85
89
|
opts.separator 'Daemon Mode:'
|
|
86
90
|
|
|
@@ -88,10 +92,6 @@ module TwirlCLI
|
|
|
88
92
|
options[:daemon] = true
|
|
89
93
|
end
|
|
90
94
|
|
|
91
|
-
opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
|
|
92
|
-
options[:no_detach] = true
|
|
93
|
-
end
|
|
94
|
-
|
|
95
95
|
opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
|
|
96
96
|
options[:daemon] = true
|
|
97
97
|
options[:daemon_name] = name
|
|
@@ -23,11 +23,12 @@ module TwirlRunner
|
|
|
23
23
|
RubyProgress::Utils.hide_cursor
|
|
24
24
|
spinner_thread = Thread.new { loop { spinner.animate } }
|
|
25
25
|
|
|
26
|
-
if $stdout.tty? && options[:stdout]
|
|
26
|
+
if $stdout.tty? && (options[:stdout] || options[:stdout_live])
|
|
27
27
|
oc = RubyProgress::OutputCapture.new(
|
|
28
28
|
command: options[:command],
|
|
29
29
|
lines: options[:output_lines] || 3,
|
|
30
|
-
position: options[:output_position] || :above
|
|
30
|
+
position: options[:output_position] || :above,
|
|
31
|
+
stream: options[:stdout] || options[:stdout_live]
|
|
31
32
|
)
|
|
32
33
|
oc.start
|
|
33
34
|
|
|
@@ -103,40 +104,6 @@ module TwirlRunner
|
|
|
103
104
|
begin
|
|
104
105
|
RubyProgress::Utils.hide_cursor
|
|
105
106
|
|
|
106
|
-
# Start job processor thread for twirl
|
|
107
|
-
job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
|
|
108
|
-
job_thread = Thread.new do
|
|
109
|
-
RubyProgress::Daemon.process_jobs(job_dir) do |job|
|
|
110
|
-
oc = RubyProgress::OutputCapture.new(
|
|
111
|
-
command: job['command'],
|
|
112
|
-
lines: options[:output_lines] || 3,
|
|
113
|
-
position: options[:output_position] || :above
|
|
114
|
-
)
|
|
115
|
-
oc.start
|
|
116
|
-
|
|
117
|
-
spinner.instance_variable_set(:@output_capture, oc)
|
|
118
|
-
oc.wait
|
|
119
|
-
captured = oc.lines.join("\n")
|
|
120
|
-
exit_status = oc.exit_status
|
|
121
|
-
spinner.instance_variable_set(:@output_capture, nil)
|
|
122
|
-
|
|
123
|
-
success = exit_status.to_i.zero?
|
|
124
|
-
if job['message']
|
|
125
|
-
RubyProgress::Utils.display_completion(
|
|
126
|
-
job['message'],
|
|
127
|
-
success: success,
|
|
128
|
-
show_checkmark: job['checkmark'] || false,
|
|
129
|
-
output_stream: :stdout,
|
|
130
|
-
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
131
|
-
)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
{ 'exit_status' => exit_status, 'output' => captured }
|
|
135
|
-
rescue StandardError
|
|
136
|
-
# ignore
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
107
|
spinner.animate until stop_requested
|
|
141
108
|
ensure
|
|
142
109
|
RubyProgress::Utils.clear_line
|
|
@@ -170,7 +137,6 @@ module TwirlRunner
|
|
|
170
137
|
end
|
|
171
138
|
end
|
|
172
139
|
|
|
173
|
-
job_thread&.kill
|
|
174
140
|
FileUtils.rm_f(pid_file)
|
|
175
141
|
end
|
|
176
142
|
end
|
|
@@ -33,13 +33,8 @@ module WormCLI
|
|
|
33
33
|
)
|
|
34
34
|
exit
|
|
35
35
|
elsif options[:daemon]
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
if options[:no_detach]
|
|
39
|
-
PrgCLI.backgroundize
|
|
40
|
-
else
|
|
41
|
-
PrgCLI.daemonize
|
|
42
|
-
end
|
|
36
|
+
# Background without detaching so worm remains visible in current terminal
|
|
37
|
+
PrgCLI.backgroundize
|
|
43
38
|
|
|
44
39
|
run_daemon_mode(options)
|
|
45
40
|
else
|
|
@@ -61,48 +56,6 @@ module WormCLI
|
|
|
61
56
|
progress = RubyProgress::Worm.new(options)
|
|
62
57
|
|
|
63
58
|
begin
|
|
64
|
-
# Start job processor thread for worm
|
|
65
|
-
job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
|
|
66
|
-
job_thread = Thread.new do
|
|
67
|
-
RubyProgress::Daemon.process_jobs(job_dir) do |job|
|
|
68
|
-
jid = job['id'] || SecureRandom.uuid
|
|
69
|
-
log_path = begin
|
|
70
|
-
File.join(File.dirname(job_dir), "#{jid}.log")
|
|
71
|
-
rescue StandardError
|
|
72
|
-
nil
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
oc = RubyProgress::OutputCapture.new(
|
|
76
|
-
command: job['command'],
|
|
77
|
-
lines: options[:output_lines] || 3,
|
|
78
|
-
position: options[:output_position] || :above,
|
|
79
|
-
log_path: log_path
|
|
80
|
-
)
|
|
81
|
-
oc.start
|
|
82
|
-
|
|
83
|
-
progress.instance_variable_set(:@output_capture, oc)
|
|
84
|
-
oc.wait
|
|
85
|
-
captured = oc.lines.join("\n")
|
|
86
|
-
exit_status = oc.exit_status
|
|
87
|
-
progress.instance_variable_set(:@output_capture, nil)
|
|
88
|
-
|
|
89
|
-
success = exit_status.to_i.zero?
|
|
90
|
-
if job['message']
|
|
91
|
-
RubyProgress::Utils.display_completion(
|
|
92
|
-
job['message'],
|
|
93
|
-
success: success,
|
|
94
|
-
show_checkmark: job['checkmark'] || false,
|
|
95
|
-
output_stream: :stdout,
|
|
96
|
-
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
97
|
-
)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
{ 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
|
|
101
|
-
rescue StandardError
|
|
102
|
-
# ignore per-job errors
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
59
|
progress.run_daemon_mode(
|
|
107
60
|
success_message: options[:success],
|
|
108
61
|
show_checkmark: options[:checkmark],
|
|
@@ -110,7 +63,6 @@ module WormCLI
|
|
|
110
63
|
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
111
64
|
)
|
|
112
65
|
ensure
|
|
113
|
-
job_thread&.kill
|
|
114
66
|
FileUtils.rm_f(pid_file)
|
|
115
67
|
end
|
|
116
68
|
end
|