ruby-progress 1.3.4 → 1.3.6
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 +26 -13
- data/CHANGELOG.md +37 -47
- data/DAEMON_MODE.md +127 -0
- data/Gemfile +7 -1
- data/Gemfile.lock +19 -18
- data/JOB_CLI_REFACTOR.md +67 -0
- data/README.md +90 -94
- data/bin/prg +10 -16
- data/blog-post.md +201 -109
- data/examples/daemon_job_example.sh +8 -10
- data/lib/ruby-progress/cli/fill_options.rb +2 -6
- data/lib/ruby-progress/cli/job_cli.rb +170 -131
- data/lib/ruby-progress/cli/ripple_cli.rb +13 -55
- data/lib/ruby-progress/cli/ripple_options.rb +14 -3
- data/lib/ruby-progress/cli/twirl_cli.rb +8 -1
- data/lib/ruby-progress/cli/twirl_options.rb +0 -4
- data/lib/ruby-progress/cli/twirl_runner.rb +9 -36
- data/lib/ruby-progress/cli/worm_cli.rb +7 -51
- data/lib/ruby-progress/cli/worm_options.rb +0 -6
- data/lib/ruby-progress/cli/worm_runner.rb +7 -1
- data/lib/ruby-progress/daemon.rb +2 -64
- data/lib/ruby-progress/fill_cli.rb +2 -72
- data/lib/ruby-progress/output_capture.rb +7 -2
- data/lib/ruby-progress/utils.rb +11 -6
- data/lib/ruby-progress/version.rb +5 -5
- data/ruby-progress.gemspec +41 -0
- data/scripts/coverage_analysis.rb +49 -0
- data/test_daemon.sh +20 -0
- metadata +6 -15
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
|
-
# Example: start a worm
|
|
2
|
+
# Example: start a worm in background mode, do some work, then stop it with a message.
|
|
3
3
|
# This script assumes you're running from the project root and have a working
|
|
4
4
|
# `bin/prg` script in the repository.
|
|
5
5
|
|
|
@@ -8,18 +8,16 @@ set -eu
|
|
|
8
8
|
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
9
9
|
PRG_BIN="$PROJECT_ROOT/bin/prg"
|
|
10
10
|
|
|
11
|
-
echo "Starting worm
|
|
12
|
-
#
|
|
13
|
-
$PRG_BIN worm --daemon-as example --message "Example
|
|
11
|
+
echo "Starting worm in background mode (named 'example')..."
|
|
12
|
+
# Use --no-detach to keep it visible in the current terminal for demos
|
|
13
|
+
$PRG_BIN worm --daemon-as example --no-detach --message "Example running..." &
|
|
14
14
|
|
|
15
|
-
sleep 0.
|
|
15
|
+
sleep 0.5
|
|
16
16
|
|
|
17
|
-
echo "
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
sleep 0.1
|
|
17
|
+
echo "Doing some work..."
|
|
18
|
+
sleep 2
|
|
21
19
|
|
|
22
20
|
echo "Stopping daemon with success message..."
|
|
23
|
-
$PRG_BIN
|
|
21
|
+
$PRG_BIN job send --daemon-name example --message "Example finished" --checkmark
|
|
24
22
|
|
|
25
23
|
echo "Done."
|
|
@@ -5,7 +5,7 @@ require 'optparse'
|
|
|
5
5
|
module RubyProgress
|
|
6
6
|
module FillCLI
|
|
7
7
|
# Option parsing extracted to reduce module length in FillCLI
|
|
8
|
-
# rubocop:disable Metrics/AbcSize
|
|
8
|
+
# rubocop:disable Metrics/AbcSize
|
|
9
9
|
module Options
|
|
10
10
|
# rubocop :disable Metrics/MethodLength
|
|
11
11
|
def self.parse_cli_options
|
|
@@ -146,10 +146,6 @@ module RubyProgress
|
|
|
146
146
|
options[:daemon_name] = name
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
-
opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
|
|
150
|
-
options[:no_detach] = true
|
|
151
|
-
end
|
|
152
|
-
|
|
153
149
|
opts.on('--pid-file FILE', 'PID file location (default: /tmp/ruby-progress/fill.pid)') do |file|
|
|
154
150
|
options[:pid_file] = file
|
|
155
151
|
end
|
|
@@ -266,6 +262,6 @@ module RubyProgress
|
|
|
266
262
|
opts.to_s
|
|
267
263
|
end
|
|
268
264
|
end
|
|
269
|
-
# rubocop:enable Metrics/AbcSize
|
|
265
|
+
# rubocop:enable Metrics/AbcSize
|
|
270
266
|
end
|
|
271
267
|
end
|
|
@@ -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(' ')
|
|
@@ -125,7 +128,7 @@ module RippleCLI
|
|
|
125
128
|
end
|
|
126
129
|
|
|
127
130
|
def self.run_daemon_mode(text, options)
|
|
128
|
-
pid_file = options
|
|
131
|
+
pid_file = resolve_pid_file(options, :daemon_name)
|
|
129
132
|
FileUtils.mkdir_p(File.dirname(pid_file))
|
|
130
133
|
File.write(pid_file, Process.pid.to_s)
|
|
131
134
|
begin
|
|
@@ -139,9 +142,6 @@ module RippleCLI
|
|
|
139
142
|
Signal.trap('TERM') { stop_requested = true }
|
|
140
143
|
Signal.trap('HUP') { stop_requested = true }
|
|
141
144
|
|
|
142
|
-
job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
|
|
143
|
-
job_thread = Thread.new { process_daemon_jobs_for_rippler(job_dir, rippler, options) }
|
|
144
|
-
|
|
145
145
|
rippler.advance until stop_requested
|
|
146
146
|
ensure
|
|
147
147
|
RubyProgress::Utils.clear_line
|
|
@@ -184,51 +184,9 @@ module RippleCLI
|
|
|
184
184
|
end
|
|
185
185
|
|
|
186
186
|
# stop job thread and cleanup
|
|
187
|
-
job_thread&.kill
|
|
188
187
|
FileUtils.rm_f(pid_file)
|
|
189
188
|
end
|
|
190
189
|
end
|
|
191
190
|
|
|
192
|
-
def self.process_daemon_jobs_for_rippler(job_dir, rippler, options)
|
|
193
|
-
RubyProgress::Daemon.process_jobs(job_dir) do |job|
|
|
194
|
-
jid = job['id'] || SecureRandom.uuid
|
|
195
|
-
log_path = begin
|
|
196
|
-
File.join(File.dirname(job_dir), "#{jid}.log")
|
|
197
|
-
rescue StandardError
|
|
198
|
-
nil
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
oc = RubyProgress::OutputCapture.new(
|
|
202
|
-
command: job['command'],
|
|
203
|
-
lines: options[:output_lines] || 3,
|
|
204
|
-
position: options[:output_position] || :above,
|
|
205
|
-
log_path: log_path
|
|
206
|
-
)
|
|
207
|
-
oc.start
|
|
208
|
-
|
|
209
|
-
rippler.instance_variable_set(:@output_capture, oc)
|
|
210
|
-
oc.wait
|
|
211
|
-
captured = oc.lines.join("\n")
|
|
212
|
-
exit_status = oc.exit_status
|
|
213
|
-
rippler.instance_variable_set(:@output_capture, nil)
|
|
214
|
-
|
|
215
|
-
success = exit_status.to_i.zero?
|
|
216
|
-
if job['message']
|
|
217
|
-
RubyProgress::Utils.display_completion(
|
|
218
|
-
job['message'],
|
|
219
|
-
success: success,
|
|
220
|
-
show_checkmark: job['checkmark'] || false,
|
|
221
|
-
output_stream: :stdout,
|
|
222
|
-
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
223
|
-
)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
{ 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
|
|
227
|
-
rescue StandardError
|
|
228
|
-
# ignore per-job errors; process_jobs will write result
|
|
229
|
-
nil
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
191
|
# Options parsing moved to ripple_options.rb
|
|
234
192
|
end
|
|
@@ -106,8 +106,9 @@ module RippleCLI
|
|
|
106
106
|
options[:daemon] = true
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
opts.on('--
|
|
110
|
-
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
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
|
|
@@ -118,10 +119,20 @@ module RippleCLI
|
|
|
118
119
|
options[:stop] = true
|
|
119
120
|
end
|
|
120
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
|
+
|
|
121
127
|
opts.on('--status', 'Show daemon status (running/not running)') do
|
|
122
128
|
options[:status] = true
|
|
123
129
|
end
|
|
124
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
|
+
|
|
125
136
|
opts.on('--stop-success MESSAGE', 'When stopping, show this success message') do |msg|
|
|
126
137
|
options[:stop_success] = msg
|
|
127
138
|
end
|
|
@@ -135,7 +146,7 @@ module RippleCLI
|
|
|
135
146
|
opts.separator ''
|
|
136
147
|
opts.separator 'Daemon notes:'
|
|
137
148
|
opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
|
|
138
|
-
opts.separator ' - Use --
|
|
149
|
+
opts.separator ' - Use --daemon-as NAME for named daemons, or --stop-id/--status-id for named control.'
|
|
139
150
|
|
|
140
151
|
opts.separator ''
|
|
141
152
|
opts.separator 'General:'
|
|
@@ -8,6 +8,11 @@ require_relative 'twirl_runner'
|
|
|
8
8
|
# Twirl CLI (extracted from bin/prg)
|
|
9
9
|
module TwirlCLI
|
|
10
10
|
def self.run
|
|
11
|
+
trap('INT') do
|
|
12
|
+
RubyProgress::Utils.show_cursor
|
|
13
|
+
exit
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
options = TwirlCLI::Options.parse_cli_options
|
|
12
17
|
|
|
13
18
|
if options[:status]
|
|
@@ -26,7 +31,9 @@ module TwirlCLI
|
|
|
26
31
|
)
|
|
27
32
|
exit
|
|
28
33
|
elsif options[:daemon]
|
|
29
|
-
|
|
34
|
+
# Background without detaching so spinner remains visible in current terminal
|
|
35
|
+
PrgCLI.backgroundize
|
|
36
|
+
|
|
30
37
|
TwirlRunner.run_daemon_mode(options)
|
|
31
38
|
elsif options[:command]
|
|
32
39
|
TwirlRunner.run_with_command(options)
|
|
@@ -92,10 +92,6 @@ module TwirlCLI
|
|
|
92
92
|
options[:daemon] = true
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
|
|
96
|
-
options[:no_detach] = true
|
|
97
|
-
end
|
|
98
|
-
|
|
99
95
|
opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
|
|
100
96
|
options[:daemon] = true
|
|
101
97
|
options[:daemon_name] = name
|
|
@@ -46,6 +46,11 @@ module TwirlRunner
|
|
|
46
46
|
|
|
47
47
|
spinner_thread.kill
|
|
48
48
|
RubyProgress::Utils.clear_line
|
|
49
|
+
rescue Interrupt
|
|
50
|
+
spinner_thread&.kill
|
|
51
|
+
RubyProgress::Utils.clear_line
|
|
52
|
+
RubyProgress::Utils.show_cursor
|
|
53
|
+
exit 130
|
|
49
54
|
ensure
|
|
50
55
|
RubyProgress::Utils.show_cursor
|
|
51
56
|
end
|
|
@@ -74,6 +79,10 @@ module TwirlRunner
|
|
|
74
79
|
begin
|
|
75
80
|
RubyProgress::Utils.hide_cursor
|
|
76
81
|
loop { spinner.animate }
|
|
82
|
+
rescue Interrupt
|
|
83
|
+
RubyProgress::Utils.clear_line
|
|
84
|
+
RubyProgress::Utils.show_cursor
|
|
85
|
+
exit 130
|
|
77
86
|
ensure
|
|
78
87
|
RubyProgress::Utils.show_cursor
|
|
79
88
|
if options[:success] || options[:checkmark]
|
|
@@ -104,41 +113,6 @@ module TwirlRunner
|
|
|
104
113
|
begin
|
|
105
114
|
RubyProgress::Utils.hide_cursor
|
|
106
115
|
|
|
107
|
-
# Start job processor thread for twirl
|
|
108
|
-
job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
|
|
109
|
-
job_thread = Thread.new do
|
|
110
|
-
RubyProgress::Daemon.process_jobs(job_dir) do |job|
|
|
111
|
-
oc = RubyProgress::OutputCapture.new(
|
|
112
|
-
command: job['command'],
|
|
113
|
-
lines: options[:output_lines] || 3,
|
|
114
|
-
position: options[:output_position] || :above,
|
|
115
|
-
stream: options[:stdout] || options[:stdout_live]
|
|
116
|
-
)
|
|
117
|
-
oc.start
|
|
118
|
-
|
|
119
|
-
spinner.instance_variable_set(:@output_capture, oc)
|
|
120
|
-
oc.wait
|
|
121
|
-
captured = oc.lines.join("\n")
|
|
122
|
-
exit_status = oc.exit_status
|
|
123
|
-
spinner.instance_variable_set(:@output_capture, nil)
|
|
124
|
-
|
|
125
|
-
success = exit_status.to_i.zero?
|
|
126
|
-
if job['message']
|
|
127
|
-
RubyProgress::Utils.display_completion(
|
|
128
|
-
job['message'],
|
|
129
|
-
success: success,
|
|
130
|
-
show_checkmark: job['checkmark'] || false,
|
|
131
|
-
output_stream: :stdout,
|
|
132
|
-
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
133
|
-
)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
{ 'exit_status' => exit_status, 'output' => captured }
|
|
137
|
-
rescue StandardError
|
|
138
|
-
# ignore
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
116
|
spinner.animate until stop_requested
|
|
143
117
|
ensure
|
|
144
118
|
RubyProgress::Utils.clear_line
|
|
@@ -172,7 +146,6 @@ module TwirlRunner
|
|
|
172
146
|
end
|
|
173
147
|
end
|
|
174
148
|
|
|
175
|
-
job_thread&.kill
|
|
176
149
|
FileUtils.rm_f(pid_file)
|
|
177
150
|
end
|
|
178
151
|
end
|