enhance_swarm 1.0.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 +7 -0
- data/.enhance_swarm/agent_scripts/frontend_agent.md +39 -0
- data/.enhance_swarm/user_patterns.json +37 -0
- data/CHANGELOG.md +184 -0
- data/LICENSE +21 -0
- data/PRODUCTION_TEST_LOG.md +502 -0
- data/README.md +905 -0
- data/Rakefile +28 -0
- data/USAGE_EXAMPLES.md +477 -0
- data/examples/enhance_workflow.md +346 -0
- data/examples/rails_project.md +253 -0
- data/exe/enhance-swarm +30 -0
- data/lib/enhance_swarm/additional_commands.rb +299 -0
- data/lib/enhance_swarm/agent_communicator.rb +460 -0
- data/lib/enhance_swarm/agent_reviewer.rb +283 -0
- data/lib/enhance_swarm/agent_spawner.rb +462 -0
- data/lib/enhance_swarm/cleanup_manager.rb +245 -0
- data/lib/enhance_swarm/cli.rb +1592 -0
- data/lib/enhance_swarm/command_executor.rb +78 -0
- data/lib/enhance_swarm/configuration.rb +324 -0
- data/lib/enhance_swarm/control_agent.rb +307 -0
- data/lib/enhance_swarm/dependency_validator.rb +195 -0
- data/lib/enhance_swarm/error_recovery.rb +785 -0
- data/lib/enhance_swarm/generator.rb +194 -0
- data/lib/enhance_swarm/interrupt_handler.rb +512 -0
- data/lib/enhance_swarm/logger.rb +106 -0
- data/lib/enhance_swarm/mcp_integration.rb +85 -0
- data/lib/enhance_swarm/monitor.rb +28 -0
- data/lib/enhance_swarm/notification_manager.rb +444 -0
- data/lib/enhance_swarm/orchestrator.rb +313 -0
- data/lib/enhance_swarm/output_streamer.rb +281 -0
- data/lib/enhance_swarm/process_monitor.rb +266 -0
- data/lib/enhance_swarm/progress_tracker.rb +215 -0
- data/lib/enhance_swarm/project_analyzer.rb +612 -0
- data/lib/enhance_swarm/resource_manager.rb +177 -0
- data/lib/enhance_swarm/retry_handler.rb +40 -0
- data/lib/enhance_swarm/session_manager.rb +247 -0
- data/lib/enhance_swarm/signal_handler.rb +95 -0
- data/lib/enhance_swarm/smart_defaults.rb +708 -0
- data/lib/enhance_swarm/task_integration.rb +150 -0
- data/lib/enhance_swarm/task_manager.rb +174 -0
- data/lib/enhance_swarm/version.rb +5 -0
- data/lib/enhance_swarm/visual_dashboard.rb +555 -0
- data/lib/enhance_swarm/web_ui.rb +211 -0
- data/lib/enhance_swarm.rb +69 -0
- data/setup.sh +86 -0
- data/sig/enhance_swarm.rbs +4 -0
- data/templates/claude/CLAUDE.md +160 -0
- data/templates/claude/MCP.md +117 -0
- data/templates/claude/PERSONAS.md +114 -0
- data/templates/claude/RULES.md +221 -0
- data/test_builtin_functionality.rb +121 -0
- data/test_core_components.rb +156 -0
- data/test_real_claude_integration.rb +285 -0
- data/test_security.rb +150 -0
- data/test_smart_defaults.rb +155 -0
- data/test_task_integration.rb +173 -0
- data/test_web_ui.rb +245 -0
- data/web/assets/css/main.css +645 -0
- data/web/assets/js/kanban.js +499 -0
- data/web/assets/js/main.js +525 -0
- data/web/templates/dashboard.html.erb +226 -0
- data/web/templates/kanban.html.erb +193 -0
- metadata +293 -0
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'logger'
|
4
|
+
require_relative 'configuration'
|
5
|
+
|
6
|
+
module EnhanceSwarm
|
7
|
+
# Manages system resources and enforces limits for agent spawning
|
8
|
+
class ResourceManager
|
9
|
+
MAX_CONCURRENT_AGENTS = 10
|
10
|
+
MAX_MEMORY_MB = 2048
|
11
|
+
MAX_DISK_MB = 1024
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@config = EnhanceSwarm.configuration
|
15
|
+
end
|
16
|
+
|
17
|
+
def can_spawn_agent?
|
18
|
+
result = {
|
19
|
+
allowed: true,
|
20
|
+
reasons: []
|
21
|
+
}
|
22
|
+
|
23
|
+
# Check concurrent agent limit
|
24
|
+
current_agents = count_active_agents
|
25
|
+
max_agents = @config.max_concurrent_agents || MAX_CONCURRENT_AGENTS
|
26
|
+
|
27
|
+
if current_agents >= max_agents
|
28
|
+
result[:allowed] = false
|
29
|
+
result[:reasons] << "Maximum concurrent agents reached (#{current_agents}/#{max_agents})"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check system memory
|
33
|
+
if memory_usage_too_high?
|
34
|
+
result[:allowed] = false
|
35
|
+
result[:reasons] << "System memory usage too high"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Check disk space
|
39
|
+
if disk_usage_too_high?
|
40
|
+
result[:allowed] = false
|
41
|
+
result[:reasons] << "Insufficient disk space"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Check system load
|
45
|
+
if system_load_too_high?
|
46
|
+
result[:allowed] = false
|
47
|
+
result[:reasons] << "System load too high"
|
48
|
+
end
|
49
|
+
|
50
|
+
result
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_resource_stats
|
54
|
+
{
|
55
|
+
active_agents: count_active_agents,
|
56
|
+
max_agents: @config.max_concurrent_agents || MAX_CONCURRENT_AGENTS,
|
57
|
+
memory_usage_mb: get_memory_usage_mb,
|
58
|
+
disk_usage_mb: get_disk_usage_mb,
|
59
|
+
system_load: get_system_load
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def enforce_limits!
|
64
|
+
stats = get_resource_stats
|
65
|
+
|
66
|
+
if stats[:active_agents] > stats[:max_agents]
|
67
|
+
Logger.warn("Agent limit exceeded: #{stats[:active_agents]}/#{stats[:max_agents]}")
|
68
|
+
cleanup_oldest_agents(stats[:active_agents] - stats[:max_agents])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def count_active_agents
|
75
|
+
# Count running enhance-swarm processes
|
76
|
+
begin
|
77
|
+
ps_output = `ps aux | grep -i enhance-swarm | grep -v grep | wc -l`.strip.to_i
|
78
|
+
# Subtract 1 for the current process
|
79
|
+
[ps_output - 1, 0].max
|
80
|
+
rescue StandardError
|
81
|
+
0
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def memory_usage_too_high?
|
86
|
+
current_usage = get_memory_usage_mb
|
87
|
+
max_usage = @config.max_memory_mb || MAX_MEMORY_MB
|
88
|
+
current_usage > max_usage
|
89
|
+
end
|
90
|
+
|
91
|
+
def disk_usage_too_high?
|
92
|
+
current_usage = get_disk_usage_mb
|
93
|
+
max_usage = @config.max_disk_mb || MAX_DISK_MB
|
94
|
+
current_usage > max_usage
|
95
|
+
end
|
96
|
+
|
97
|
+
def system_load_too_high?
|
98
|
+
load_avg = get_system_load
|
99
|
+
# Consider load too high if 1-minute average > number of CPU cores
|
100
|
+
cpu_count = get_cpu_count
|
101
|
+
load_avg > cpu_count * 1.5
|
102
|
+
end
|
103
|
+
|
104
|
+
def get_memory_usage_mb
|
105
|
+
begin
|
106
|
+
# Get RSS memory usage of all enhance-swarm processes
|
107
|
+
ps_output = `ps aux | grep -i enhance-swarm | grep -v grep | awk '{sum += $6} END {print sum}'`.strip.to_i
|
108
|
+
# Convert from KB to MB
|
109
|
+
ps_output / 1024
|
110
|
+
rescue StandardError
|
111
|
+
0
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def get_disk_usage_mb
|
116
|
+
begin
|
117
|
+
# Check disk usage of .enhance_swarm directory
|
118
|
+
if Dir.exist?('.enhance_swarm')
|
119
|
+
du_output = `du -sm .enhance_swarm 2>/dev/null | awk '{print $1}'`.strip.to_i
|
120
|
+
return du_output
|
121
|
+
end
|
122
|
+
0
|
123
|
+
rescue StandardError
|
124
|
+
0
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def get_system_load
|
129
|
+
begin
|
130
|
+
# Get 1-minute load average
|
131
|
+
uptime_output = `uptime`.strip
|
132
|
+
load_match = uptime_output.match(/load averages?: ([\d.]+)/)
|
133
|
+
load_match ? load_match[1].to_f : 0.0
|
134
|
+
rescue StandardError
|
135
|
+
0.0
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def get_cpu_count
|
140
|
+
begin
|
141
|
+
# Get number of CPU cores
|
142
|
+
if RUBY_PLATFORM.include?('darwin') # macOS
|
143
|
+
`sysctl -n hw.ncpu`.strip.to_i
|
144
|
+
else # Linux
|
145
|
+
`nproc`.strip.to_i
|
146
|
+
end
|
147
|
+
rescue StandardError
|
148
|
+
4 # Default fallback
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def cleanup_oldest_agents(count)
|
153
|
+
Logger.info("Cleaning up #{count} oldest agents to enforce limits")
|
154
|
+
|
155
|
+
begin
|
156
|
+
# Get list of enhance-swarm processes sorted by start time
|
157
|
+
ps_output = `ps aux | grep -i enhance-swarm | grep -v grep | sort -k9`
|
158
|
+
pids_to_kill = ps_output.lines.first(count).map do |line|
|
159
|
+
line.split[1].to_i # Get PID
|
160
|
+
end
|
161
|
+
|
162
|
+
pids_to_kill.each do |pid|
|
163
|
+
begin
|
164
|
+
Process.kill('TERM', pid)
|
165
|
+
Logger.info("Terminated agent process: #{pid}")
|
166
|
+
rescue Errno::ESRCH
|
167
|
+
# Process already terminated
|
168
|
+
rescue StandardError => e
|
169
|
+
Logger.error("Failed to terminate process #{pid}: #{e.message}")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
rescue StandardError => e
|
173
|
+
Logger.error("Failed to cleanup agents: #{e.message}")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EnhanceSwarm
|
4
|
+
class RetryHandler
|
5
|
+
class RetryError < StandardError; end
|
6
|
+
|
7
|
+
def self.with_retry(max_retries: 3, base_delay: 1, max_delay: 30, &block)
|
8
|
+
retries = 0
|
9
|
+
|
10
|
+
begin
|
11
|
+
yield
|
12
|
+
rescue StandardError => e
|
13
|
+
retries += 1
|
14
|
+
|
15
|
+
if retries <= max_retries && retryable_error?(e)
|
16
|
+
delay = [base_delay * (2 ** (retries - 1)), max_delay].min
|
17
|
+
puts "Attempt #{retries} failed: #{e.message}. Retrying in #{delay}s...".colorize(:yellow)
|
18
|
+
sleep(delay)
|
19
|
+
retry
|
20
|
+
end
|
21
|
+
|
22
|
+
raise RetryError.new("Operation failed after #{max_retries} retries: #{e.message}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.retryable_error?(error)
|
27
|
+
case error
|
28
|
+
when CommandExecutor::CommandError
|
29
|
+
# Retry on timeout or command not found, but not on validation errors
|
30
|
+
error.message.include?('timed out') || error.message.include?('not found')
|
31
|
+
when Errno::ENOENT, Errno::ECONNREFUSED, Errno::ETIMEDOUT
|
32
|
+
true
|
33
|
+
when IOError, SystemCallError
|
34
|
+
true
|
35
|
+
else
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,247 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'securerandom'
|
6
|
+
require_relative 'logger'
|
7
|
+
|
8
|
+
module EnhanceSwarm
|
9
|
+
class SessionManager
|
10
|
+
SESSION_DIR = '.enhance_swarm'
|
11
|
+
SESSION_FILE = 'session.json'
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@session_path = File.join(Dir.pwd, SESSION_DIR, SESSION_FILE)
|
15
|
+
ensure_session_directory
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_session(task_description = nil)
|
19
|
+
session_data = {
|
20
|
+
session_id: generate_session_id,
|
21
|
+
start_time: Time.now.iso8601,
|
22
|
+
task_description: task_description,
|
23
|
+
agents: [],
|
24
|
+
status: 'active'
|
25
|
+
}
|
26
|
+
|
27
|
+
write_session(session_data)
|
28
|
+
Logger.info("Created new session: #{session_data[:session_id]}")
|
29
|
+
session_data
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_agent(role, pid, worktree_path, task = nil)
|
33
|
+
session = read_session
|
34
|
+
return false unless session
|
35
|
+
|
36
|
+
agent_data = {
|
37
|
+
role: role,
|
38
|
+
pid: pid,
|
39
|
+
worktree_path: worktree_path,
|
40
|
+
task: task,
|
41
|
+
start_time: Time.now.iso8601,
|
42
|
+
status: 'running'
|
43
|
+
}
|
44
|
+
|
45
|
+
session[:agents] << agent_data
|
46
|
+
write_session(session)
|
47
|
+
Logger.info("Added agent to session: #{role} (PID: #{pid})")
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_agent_status(pid, status, completion_time = nil)
|
52
|
+
session = read_session
|
53
|
+
return false unless session
|
54
|
+
|
55
|
+
agent = session[:agents].find { |a| a[:pid] == pid }
|
56
|
+
return false unless agent
|
57
|
+
|
58
|
+
agent[:status] = status
|
59
|
+
agent[:completion_time] = completion_time if completion_time
|
60
|
+
|
61
|
+
write_session(session)
|
62
|
+
Logger.info("Updated agent status: PID #{pid} -> #{status}")
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def remove_agent(pid)
|
67
|
+
session = read_session
|
68
|
+
return false unless session
|
69
|
+
|
70
|
+
initial_count = session[:agents].length
|
71
|
+
session[:agents].reject! { |a| a[:pid] == pid }
|
72
|
+
|
73
|
+
if session[:agents].length < initial_count
|
74
|
+
write_session(session)
|
75
|
+
Logger.info("Removed agent from session: PID #{pid}")
|
76
|
+
true
|
77
|
+
else
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_active_agents
|
83
|
+
session = read_session
|
84
|
+
return [] unless session
|
85
|
+
|
86
|
+
session[:agents].select { |a| a[:status] == 'running' }
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_all_agents
|
90
|
+
session = read_session
|
91
|
+
return [] unless session
|
92
|
+
|
93
|
+
session[:agents] || []
|
94
|
+
end
|
95
|
+
|
96
|
+
def session_exists?
|
97
|
+
File.exist?(@session_path)
|
98
|
+
end
|
99
|
+
|
100
|
+
def read_session
|
101
|
+
return nil unless session_exists?
|
102
|
+
|
103
|
+
begin
|
104
|
+
content = File.read(@session_path)
|
105
|
+
JSON.parse(content, symbolize_names: true)
|
106
|
+
rescue JSON::ParserError, StandardError => e
|
107
|
+
Logger.error("Failed to read session file: #{e.message}")
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def session_status
|
113
|
+
session = read_session
|
114
|
+
return { exists: false } unless session
|
115
|
+
|
116
|
+
active_agents = get_active_agents
|
117
|
+
all_agents = get_all_agents
|
118
|
+
|
119
|
+
{
|
120
|
+
exists: true,
|
121
|
+
session_id: session[:session_id],
|
122
|
+
start_time: session[:start_time],
|
123
|
+
task_description: session[:task_description],
|
124
|
+
status: session[:status],
|
125
|
+
total_agents: all_agents.length,
|
126
|
+
active_agents: active_agents.length,
|
127
|
+
completed_agents: all_agents.count { |a| a[:status] == 'completed' },
|
128
|
+
failed_agents: all_agents.count { |a| a[:status] == 'failed' },
|
129
|
+
agents: all_agents
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
def close_session
|
134
|
+
session = read_session
|
135
|
+
return false unless session
|
136
|
+
|
137
|
+
session[:status] = 'completed'
|
138
|
+
session[:end_time] = Time.now.iso8601
|
139
|
+
write_session(session)
|
140
|
+
Logger.info("Closed session: #{session[:session_id]}")
|
141
|
+
true
|
142
|
+
end
|
143
|
+
|
144
|
+
def cleanup_session
|
145
|
+
return false unless session_exists?
|
146
|
+
|
147
|
+
# Archive the session before removing
|
148
|
+
if archive_session
|
149
|
+
File.delete(@session_path)
|
150
|
+
Logger.info("Cleaned up session file")
|
151
|
+
true
|
152
|
+
else
|
153
|
+
false
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def check_agent_processes
|
158
|
+
session = read_session
|
159
|
+
return [] unless session
|
160
|
+
|
161
|
+
updated_agents = []
|
162
|
+
session_changed = false
|
163
|
+
|
164
|
+
session[:agents].each do |agent|
|
165
|
+
next unless agent[:status] == 'running'
|
166
|
+
|
167
|
+
# Check if process is still running
|
168
|
+
if process_running?(agent[:pid])
|
169
|
+
updated_agents << agent
|
170
|
+
else
|
171
|
+
# Process is no longer running, update status
|
172
|
+
agent[:status] = 'stopped'
|
173
|
+
agent[:completion_time] = Time.now.iso8601
|
174
|
+
session_changed = true
|
175
|
+
Logger.info("Agent process stopped: #{agent[:role]} (PID: #{agent[:pid]})")
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
write_session(session) if session_changed
|
180
|
+
updated_agents
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
def ensure_session_directory
|
186
|
+
session_dir = File.dirname(@session_path)
|
187
|
+
FileUtils.mkdir_p(session_dir) unless Dir.exist?(session_dir)
|
188
|
+
end
|
189
|
+
|
190
|
+
def write_session(session_data)
|
191
|
+
begin
|
192
|
+
File.write(@session_path, JSON.pretty_generate(session_data))
|
193
|
+
true
|
194
|
+
rescue StandardError => e
|
195
|
+
Logger.error("Failed to write session file: #{e.message}")
|
196
|
+
false
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def generate_session_id
|
201
|
+
Time.now.to_i.to_s + '_' + SecureRandom.hex(4)
|
202
|
+
end
|
203
|
+
|
204
|
+
def archive_session
|
205
|
+
return true unless session_exists?
|
206
|
+
|
207
|
+
begin
|
208
|
+
session = read_session
|
209
|
+
return false unless session
|
210
|
+
|
211
|
+
# Create archives directory
|
212
|
+
archive_dir = File.join(Dir.pwd, SESSION_DIR, 'archives')
|
213
|
+
FileUtils.mkdir_p(archive_dir)
|
214
|
+
|
215
|
+
# Create archive filename with session ID and timestamp
|
216
|
+
archive_name = "session_#{session[:session_id]}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.json"
|
217
|
+
archive_path = File.join(archive_dir, archive_name)
|
218
|
+
|
219
|
+
FileUtils.cp(@session_path, archive_path)
|
220
|
+
Logger.info("Archived session to: #{archive_path}")
|
221
|
+
true
|
222
|
+
rescue StandardError => e
|
223
|
+
Logger.error("Failed to archive session: #{e.message}")
|
224
|
+
false
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def process_running?(pid)
|
229
|
+
begin
|
230
|
+
# Use Process.kill(0, pid) to check if process exists
|
231
|
+
# This doesn't actually kill the process, just checks if it's running
|
232
|
+
Process.kill(0, pid.to_i)
|
233
|
+
true
|
234
|
+
rescue Errno::ESRCH
|
235
|
+
# Process doesn't exist
|
236
|
+
false
|
237
|
+
rescue Errno::EPERM
|
238
|
+
# Process exists but we don't have permission to signal it
|
239
|
+
# This means it's running but owned by another user
|
240
|
+
true
|
241
|
+
rescue StandardError
|
242
|
+
# Any other error, assume process is not running
|
243
|
+
false
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'logger'
|
4
|
+
require_relative 'cleanup_manager'
|
5
|
+
|
6
|
+
module EnhanceSwarm
|
7
|
+
class SignalHandler
|
8
|
+
def self.setup
|
9
|
+
@shutdown_requested = false
|
10
|
+
@active_operations = {}
|
11
|
+
|
12
|
+
# Handle graceful shutdown signals
|
13
|
+
Signal.trap('INT') { handle_shutdown('SIGINT') }
|
14
|
+
Signal.trap('TERM') { handle_shutdown('SIGTERM') }
|
15
|
+
|
16
|
+
# Handle info signal for status
|
17
|
+
if Signal.list.key?('USR1')
|
18
|
+
Signal.trap('USR1') { handle_status_request }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.handle_shutdown(signal)
|
23
|
+
return if @shutdown_requested
|
24
|
+
|
25
|
+
@shutdown_requested = true
|
26
|
+
Logger.info("Received #{signal}, initiating graceful shutdown...")
|
27
|
+
|
28
|
+
begin
|
29
|
+
# Stop accepting new operations
|
30
|
+
puts "\nš Graceful shutdown initiated...".colorize(:yellow)
|
31
|
+
|
32
|
+
# Clean up any active operations
|
33
|
+
cleanup_active_operations
|
34
|
+
|
35
|
+
# Perform final cleanup
|
36
|
+
CleanupManager.cleanup_all_swarm_resources
|
37
|
+
|
38
|
+
Logger.info("Graceful shutdown completed")
|
39
|
+
puts "ā
Shutdown complete".colorize(:green)
|
40
|
+
|
41
|
+
exit(0)
|
42
|
+
rescue StandardError => e
|
43
|
+
Logger.error("Error during shutdown: #{e.message}")
|
44
|
+
puts "ā Error during shutdown: #{e.message}".colorize(:red)
|
45
|
+
exit(1)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.handle_status_request
|
50
|
+
Logger.info("Status request received via USR1")
|
51
|
+
|
52
|
+
status = {
|
53
|
+
active_operations: @active_operations.size,
|
54
|
+
shutdown_requested: @shutdown_requested,
|
55
|
+
timestamp: Time.now.iso8601
|
56
|
+
}
|
57
|
+
|
58
|
+
puts JSON.pretty_generate(status)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.register_operation(operation_id, details = {})
|
62
|
+
@active_operations ||= {}
|
63
|
+
@active_operations[operation_id] = {
|
64
|
+
started_at: Time.now,
|
65
|
+
details: details
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.unregister_operation(operation_id)
|
70
|
+
@active_operations&.delete(operation_id)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.shutdown_requested?
|
74
|
+
@shutdown_requested
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def self.cleanup_active_operations
|
80
|
+
return unless @active_operations&.any?
|
81
|
+
|
82
|
+
Logger.info("Cleaning up #{@active_operations.size} active operations")
|
83
|
+
|
84
|
+
@active_operations.each do |operation_id, details|
|
85
|
+
begin
|
86
|
+
CleanupManager.cleanup_failed_operation(operation_id, details[:details])
|
87
|
+
rescue StandardError => e
|
88
|
+
Logger.error("Failed to cleanup operation #{operation_id}: #{e.message}")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
@active_operations.clear
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|