consolle 0.2.6 → 0.2.8

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.
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pty"
4
- require "timeout"
5
- require "fcntl"
6
- require "logger"
3
+ require 'pty'
4
+ require 'timeout'
5
+ require 'fcntl'
6
+ require 'logger'
7
7
 
8
8
  # Ruby 3.4.0+ extracts base64 as a default gem
9
9
  # Suppress warning by silencing verbose mode temporarily
10
10
  original_verbose = $VERBOSE
11
11
  $VERBOSE = nil
12
- require "base64"
12
+ require 'base64'
13
13
  $VERBOSE = original_verbose
14
14
 
15
15
  module Consolle
@@ -19,17 +19,17 @@ module Consolle
19
19
 
20
20
  RESTART_DELAY = 1 # seconds
21
21
  MAX_RESTARTS = 5 # within 5 minutes
22
- RESTART_WINDOW = 300 # 5 minutes
22
+ RESTART_WINDOW = 300 # 5 minutes
23
23
  # Match various Rails console prompts
24
24
  # Match various console prompts: custom sentinel, Rails app prompts, IRB prompts, and generic prompts
25
25
  # Allow optional non-word characters before the prompt (e.g., Unicode symbols like ▽)
26
- PROMPT_PATTERN = /^[^\w]*(\u001E\u001F<CONSOLLE>\u001F\u001E|\w+[-_]?\w*\([^)]*\)>|irb\([^)]+\):[\d]+:?[\d]*[>*]|>>|>)\s*$/
26
+ PROMPT_PATTERN = /^[^\w]*(\u001E\u001F<CONSOLLE>\u001F\u001E|\w+[-_]?\w*\([^)]*\)>|irb\([^)]+\):\d+:?\d*[>*]|>>|>)\s*$/
27
27
  CTRL_C = "\x03"
28
28
 
29
- def initialize(rails_root:, rails_env: "development", logger: nil, command: nil)
29
+ def initialize(rails_root:, rails_env: 'development', logger: nil, command: nil)
30
30
  @rails_root = rails_root
31
31
  @rails_env = rails_env
32
- @command = command || "bin/rails console"
32
+ @command = command || 'bin/rails console'
33
33
  @logger = logger || Logger.new(STDOUT)
34
34
  @pid = nil
35
35
  @reader = nil
@@ -38,27 +38,27 @@ module Consolle
38
38
  @restart_timestamps = []
39
39
  @watchdog_thread = nil
40
40
  @mutex = Mutex.new
41
- @process_mutex = Mutex.new # Separate mutex for process lifecycle management
42
-
41
+ @process_mutex = Mutex.new # Separate mutex for process lifecycle management
42
+
43
43
  spawn_console
44
44
  start_watchdog
45
45
  end
46
46
 
47
47
  def eval(code, timeout: 30)
48
48
  @mutex.synchronize do
49
- raise "Console is not running" unless running?
50
-
49
+ raise 'Console is not running' unless running?
50
+
51
51
  # Check if this is a remote console
52
- is_remote = @command.include?("ssh") || @command.include?("kamal") || @command.include?("docker")
53
-
52
+ is_remote = @command.include?('ssh') || @command.include?('kamal') || @command.include?('docker')
53
+
54
54
  if is_remote
55
55
  # Send Ctrl-C to ensure clean state before execution
56
56
  @writer.write(CTRL_C)
57
57
  @writer.flush
58
-
58
+
59
59
  # Clear any pending output
60
60
  clear_buffer
61
-
61
+
62
62
  # Wait for prompt after Ctrl-C
63
63
  begin
64
64
  wait_for_prompt(timeout: 1, consume_all: true)
@@ -69,22 +69,42 @@ module Consolle
69
69
  # For local consoles, just clear buffer
70
70
  clear_buffer
71
71
  end
72
-
72
+
73
73
  # Encode code using Base64 to handle special characters and remote consoles
74
74
  # Ensure UTF-8 encoding to handle strings that may be tagged as ASCII-8BIT
75
- utf8_code = code.encoding == Encoding::UTF_8 ? code : code.dup.force_encoding('UTF-8')
75
+ utf8_code = if code.encoding == Encoding::UTF_8
76
+ code
77
+ else
78
+ # Try to handle ASCII-8BIT strings that might contain UTF-8 data
79
+ temp = code.dup
80
+ if code.encoding == Encoding::ASCII_8BIT
81
+ # First try to interpret as UTF-8
82
+ temp.force_encoding('UTF-8')
83
+ if temp.valid_encoding?
84
+ temp
85
+ else
86
+ # If not valid UTF-8, try other common encodings
87
+ # or use replacement characters
88
+ code.encode('UTF-8', invalid: :replace, undef: :replace)
89
+ end
90
+ else
91
+ # For other encodings, convert to UTF-8
92
+ code.encode('UTF-8')
93
+ end
94
+ end
76
95
  encoded_code = Base64.strict_encode64(utf8_code)
77
-
96
+
78
97
  # Use eval to execute the Base64-decoded code
79
- eval_command = "eval(Base64.decode64('#{encoded_code}'), IRB.CurrentContext.workspace.binding)"
80
- logger.debug "[ConsoleSupervisor] Sending eval command (Base64 encoded)"
98
+ # Ensure decoded string is properly handled as UTF-8
99
+ eval_command = "eval(Base64.decode64('#{encoded_code}').force_encoding('UTF-8'), IRB.CurrentContext.workspace.binding)"
100
+ logger.debug '[ConsoleSupervisor] Sending eval command (Base64 encoded)'
81
101
  @writer.puts eval_command
82
102
  @writer.flush
83
-
103
+
84
104
  # Collect output
85
- output = +""
105
+ output = +''
86
106
  deadline = Time.now + timeout
87
-
107
+
88
108
  begin
89
109
  loop do
90
110
  if Time.now > deadline
@@ -93,17 +113,17 @@ module Consolle
93
113
  @writer.flush
94
114
  sleep 0.5
95
115
  clear_buffer
96
- return {
97
- success: false,
116
+ return {
117
+ success: false,
98
118
  output: "Execution timed out after #{timeout} seconds",
99
- execution_time: timeout
119
+ execution_time: timeout
100
120
  }
101
121
  end
102
-
122
+
103
123
  begin
104
124
  chunk = @reader.read_nonblock(4096)
105
125
  output << chunk
106
-
126
+
107
127
  # Check if we got prompt back
108
128
  clean = strip_ansi(output)
109
129
  if clean.match?(PROMPT_PATTERN)
@@ -122,21 +142,21 @@ module Consolle
122
142
  # PTY can throw EIO when no data available
123
143
  IO.select([@reader], nil, nil, 0.1)
124
144
  rescue EOFError
125
- return {
126
- success: false,
127
- output: "Console terminated",
128
- execution_time: nil
145
+ return {
146
+ success: false,
147
+ output: 'Console terminated',
148
+ execution_time: nil
129
149
  }
130
150
  end
131
151
  end
132
-
152
+
133
153
  # Parse and return result
134
154
  result = parse_output(output, eval_command)
135
-
155
+
136
156
  # Log for debugging object output issues
137
157
  logger.debug "[ConsoleSupervisor] Raw output: #{output.inspect}"
138
158
  logger.debug "[ConsoleSupervisor] Parsed result: #{result.inspect}"
139
-
159
+
140
160
  { success: true, output: result, execution_time: nil }
141
161
  rescue StandardError => e
142
162
  logger.error "[ConsoleSupervisor] Eval error: #{e.message}"
@@ -147,7 +167,7 @@ module Consolle
147
167
 
148
168
  def running?
149
169
  return false unless @pid
150
-
170
+
151
171
  begin
152
172
  Process.kill(0, @pid)
153
173
  true
@@ -158,27 +178,27 @@ module Consolle
158
178
 
159
179
  def stop
160
180
  @running = false
161
-
181
+
162
182
  # Stop watchdog first to prevent it from restarting the process
163
183
  @watchdog_thread&.kill
164
184
  @watchdog_thread&.join(1)
165
-
185
+
166
186
  # Use process mutex for clean shutdown
167
187
  @process_mutex.synchronize do
168
188
  stop_console
169
189
  end
170
-
171
- logger.info "[ConsoleSupervisor] Stopped"
190
+
191
+ logger.info '[ConsoleSupervisor] Stopped'
172
192
  end
173
193
 
174
194
  def restart
175
- logger.info "[ConsoleSupervisor] Restarting Rails console subprocess..."
176
-
195
+ logger.info '[ConsoleSupervisor] Restarting Rails console subprocess...'
196
+
177
197
  @process_mutex.synchronize do
178
198
  stop_console
179
199
  spawn_console
180
200
  end
181
-
201
+
182
202
  @pid
183
203
  end
184
204
 
@@ -186,77 +206,77 @@ module Consolle
186
206
 
187
207
  def spawn_console
188
208
  env = {
189
- "RAILS_ENV" => @rails_env,
190
-
209
+ 'RAILS_ENV' => @rails_env,
210
+
191
211
  # Skip IRB configuration file (prevent conflicts with existing settings)
192
- "IRBRC" => "skip",
193
-
212
+ 'IRBRC' => 'skip',
213
+
194
214
  # Disable pry-rails (force IRB instead of Pry)
195
- "DISABLE_PRY_RAILS" => "1",
196
-
215
+ 'DISABLE_PRY_RAILS' => '1',
216
+
197
217
  # Completely disable pager
198
- "PAGER" => "cat", # Set pager to cat (immediate output)
199
- "NO_PAGER" => "1", # Pager disable flag
200
- "LESS" => "", # Clear less pager options
201
-
218
+ 'PAGER' => 'cat', # Set pager to cat (immediate output)
219
+ 'NO_PAGER' => '1', # Pager disable flag
220
+ 'LESS' => '', # Clear less pager options
221
+
202
222
  # Terminal settings (minimal features only)
203
- "TERM" => "dumb", # Set to dumb terminal (minimize color/pager features)
204
-
223
+ 'TERM' => 'dumb', # Set to dumb terminal (minimize color/pager features)
224
+
205
225
  # Disable Rails/ActiveSupport log colors
206
- "FORCE_COLOR" => "0", # Force disable colors
207
- "NO_COLOR" => "1", # Completely disable color output
208
-
226
+ 'FORCE_COLOR' => '0', # Force disable colors
227
+ 'NO_COLOR' => '1', # Completely disable color output
228
+
209
229
  # Disable other interactive features
210
- "COLUMNS" => "120", # Fixed column count (prevent auto-detection)
211
- "LINES" => "24" # Fixed line count (prevent auto-detection)
230
+ 'COLUMNS' => '120', # Fixed column count (prevent auto-detection)
231
+ 'LINES' => '24' # Fixed line count (prevent auto-detection)
212
232
  }
213
233
  logger.info "[ConsoleSupervisor] Spawning console with command: #{@command} (#{@rails_env})"
214
-
234
+
215
235
  @reader, @writer, @pid = PTY.spawn(env, @command, chdir: @rails_root)
216
-
236
+
217
237
  # Non-blocking mode
218
238
  @reader.sync = @writer.sync = true
219
239
  flags = @reader.fcntl(Fcntl::F_GETFL, 0)
220
240
  @reader.fcntl(Fcntl::F_SETFL, flags | Fcntl::O_NONBLOCK)
221
-
241
+
222
242
  @running = true
223
-
243
+
224
244
  # Record restart timestamp
225
245
  @restart_timestamps << Time.now
226
246
  trim_restart_history
227
-
247
+
228
248
  # Wait for initial prompt
229
249
  wait_for_prompt(timeout: 15)
230
-
250
+
231
251
  # Configure IRB settings for automation
232
252
  configure_irb_for_automation
233
-
253
+
234
254
  # For remote consoles (like kamal), we need more aggressive initialization
235
255
  # Check if this looks like a remote console based on the command
236
- is_remote = @command.include?("ssh") || @command.include?("kamal") || @command.include?("docker")
237
-
256
+ is_remote = @command.include?('ssh') || @command.include?('kamal') || @command.include?('docker')
257
+
238
258
  if is_remote
239
259
  # Send Ctrl-C to ensure clean state
240
260
  @writer.write(CTRL_C)
241
261
  @writer.flush
242
-
262
+
243
263
  # Wait for prompt after Ctrl-C
244
264
  begin
245
265
  wait_for_prompt(timeout: 2, consume_all: true)
246
266
  rescue Timeout::Error
247
- logger.warn "[ConsoleSupervisor] No prompt after Ctrl-C, continuing anyway"
267
+ logger.warn '[ConsoleSupervisor] No prompt after Ctrl-C, continuing anyway'
248
268
  end
249
-
269
+
250
270
  # Send a unique marker command to ensure all initialization output is consumed
251
271
  marker = "__consolle_init_#{Time.now.to_f}__"
252
272
  @writer.puts "puts '#{marker}'"
253
273
  @writer.flush
254
-
274
+
255
275
  # Read until we see our marker
256
- output = +""
276
+ output = +''
257
277
  deadline = Time.now + 3
258
278
  marker_found = false
259
-
279
+
260
280
  while Time.now < deadline && !marker_found
261
281
  begin
262
282
  chunk = @reader.read_nonblock(4096)
@@ -268,11 +288,9 @@ module Consolle
268
288
  IO.select([@reader], nil, nil, 0.1)
269
289
  end
270
290
  end
271
-
272
- unless marker_found
273
- logger.warn "[ConsoleSupervisor] Initialization marker not found, continuing anyway"
274
- end
275
-
291
+
292
+ logger.warn '[ConsoleSupervisor] Initialization marker not found, continuing anyway' unless marker_found
293
+
276
294
  # Final cleanup for remote consoles
277
295
  @writer.write(CTRL_C)
278
296
  @writer.flush
@@ -281,7 +299,7 @@ module Consolle
281
299
  # For local consoles, minimal cleanup is sufficient
282
300
  clear_buffer
283
301
  end
284
-
302
+
285
303
  logger.info "[ConsoleSupervisor] Rails console started (PID: #{@pid})"
286
304
  rescue StandardError => e
287
305
  logger.error "[ConsoleSupervisor] Failed to spawn console: #{e.message}"
@@ -290,33 +308,35 @@ module Consolle
290
308
 
291
309
  def start_watchdog
292
310
  @watchdog_thread = Thread.new do
293
- Thread.current[:consolle_watchdog] = true # Tag thread for test cleanup
311
+ Thread.current[:consolle_watchdog] = true # Tag thread for test cleanup
294
312
  while @running
295
313
  begin
296
314
  sleep 0.5
297
-
315
+
298
316
  # Use process mutex to avoid race conditions with restart
299
317
  @process_mutex.synchronize do
300
318
  # Check if process is still alive
301
- dead_pid = Process.waitpid(@pid, Process::WNOHANG) rescue nil
302
-
303
- if dead_pid || !running?
304
- if @running # Only restart if we're supposed to be running
305
- logger.warn "[ConsoleSupervisor] Console process died (PID: #{@pid}), restarting..."
306
-
307
- # Wait before restart
308
- sleep RESTART_DELAY
309
-
310
- # Respawn
311
- spawn_console
312
- end
319
+ dead_pid = begin
320
+ Process.waitpid(@pid, Process::WNOHANG)
321
+ rescue StandardError
322
+ nil
323
+ end
324
+
325
+ if (dead_pid || !running?) && @running # Only restart if we're supposed to be running
326
+ logger.warn "[ConsoleSupervisor] Console process died (PID: #{@pid}), restarting..."
327
+
328
+ # Wait before restart
329
+ sleep RESTART_DELAY
330
+
331
+ # Respawn
332
+ spawn_console
313
333
  end
314
334
  end
315
335
  rescue Errno::ECHILD
316
336
  # Process already reaped
317
337
  @process_mutex.synchronize do
318
338
  if @running
319
- logger.warn "[ConsoleSupervisor] Console process missing, restarting..."
339
+ logger.warn '[ConsoleSupervisor] Console process missing, restarting...'
320
340
  sleep RESTART_DELAY
321
341
  spawn_console
322
342
  end
@@ -325,45 +345,45 @@ module Consolle
325
345
  logger.error "[ConsoleSupervisor] Watchdog error: #{e.message}"
326
346
  end
327
347
  end
328
-
329
- logger.info "[ConsoleSupervisor] Watchdog thread stopped"
348
+
349
+ logger.info '[ConsoleSupervisor] Watchdog thread stopped'
330
350
  end
331
351
  end
332
352
 
333
353
  def wait_for_prompt(timeout: 15, consume_all: false)
334
- output = +""
354
+ output = +''
335
355
  deadline = Time.now + timeout
336
356
  prompt_found = false
337
357
  last_data_time = Time.now
338
-
358
+
339
359
  loop do
340
360
  if Time.now > deadline
341
361
  logger.error "[ConsoleSupervisor] Output so far: #{output.inspect}"
342
362
  logger.error "[ConsoleSupervisor] Stripped: #{strip_ansi(output).inspect}"
343
363
  raise Timeout::Error, "No prompt after #{timeout} seconds"
344
364
  end
345
-
365
+
346
366
  # If we found prompt and consume_all is true, continue reading for a bit more
347
367
  if prompt_found && consume_all
348
368
  if Time.now - last_data_time > 0.5
349
- logger.info "[ConsoleSupervisor] No more data for 0.5s after prompt, stopping"
369
+ logger.info '[ConsoleSupervisor] No more data for 0.5s after prompt, stopping'
350
370
  return true
351
371
  end
352
372
  elsif prompt_found
353
373
  return true
354
374
  end
355
-
375
+
356
376
  begin
357
377
  chunk = @reader.read_nonblock(4096)
358
378
  output << chunk
359
379
  last_data_time = Time.now
360
380
  logger.debug "[ConsoleSupervisor] Got chunk: #{chunk.inspect}"
361
-
381
+
362
382
  clean = strip_ansi(output)
363
383
  # Check each line for prompt pattern
364
384
  clean.lines.each do |line|
365
385
  if line.match?(PROMPT_PATTERN)
366
- logger.info "[ConsoleSupervisor] Found prompt!"
386
+ logger.info '[ConsoleSupervisor] Found prompt!'
367
387
  prompt_found = true
368
388
  end
369
389
  end
@@ -386,49 +406,47 @@ module Consolle
386
406
  rescue IO::WaitReadable, Errno::EIO
387
407
  # Buffer cleared for this iteration
388
408
  end
389
- sleep 0.05 if i < 2 # Sleep between iterations except the last
409
+ sleep 0.05 if i < 2 # Sleep between iterations except the last
390
410
  end
391
411
  end
392
412
 
393
413
  def configure_irb_for_automation
394
414
  # Create the invisible-wrapper sentinel prompt
395
415
  sentinel_prompt = "\u001E\u001F<CONSOLLE>\u001F\u001E "
396
-
416
+
397
417
  # Send IRB configuration commands to disable interactive features
398
418
  irb_commands = [
399
419
  # Configure custom prompt mode to eliminate continuation prompts
400
- "IRB.conf[:PROMPT][:CONSOLLE] = { " \
401
- "AUTO_INDENT: false, " \
420
+ 'IRB.conf[:PROMPT][:CONSOLLE] = { ' \
421
+ 'AUTO_INDENT: false, ' \
402
422
  "PROMPT_I: #{sentinel_prompt.inspect}, " \
403
423
  "PROMPT_N: '', " \
404
424
  "PROMPT_S: '', " \
405
425
  "PROMPT_C: '', " \
406
- "RETURN: \"=> %s\\n\" }",
407
- "IRB.conf[:PROMPT_MODE] = :CONSOLLE",
408
-
426
+ 'RETURN: "=> %s\\n" }',
427
+ 'IRB.conf[:PROMPT_MODE] = :CONSOLLE',
428
+
409
429
  # Disable interactive features
410
- "IRB.conf[:USE_PAGER] = false", # Disable pager
411
- "IRB.conf[:USE_COLORIZE] = false", # Disable color output
412
- "IRB.conf[:USE_AUTOCOMPLETE] = false", # Disable autocompletion
413
- "IRB.conf[:USE_MULTILINE] = false", # Disable multiline editor to process code at once
414
- "ActiveSupport::LogSubscriber.colorize_logging = false if defined?(ActiveSupport::LogSubscriber)" # Disable Rails logging colors
430
+ 'IRB.conf[:USE_PAGER] = false', # Disable pager
431
+ 'IRB.conf[:USE_COLORIZE] = false', # Disable color output
432
+ 'IRB.conf[:USE_AUTOCOMPLETE] = false', # Disable autocompletion
433
+ 'IRB.conf[:USE_MULTILINE] = false', # Disable multiline editor to process code at once
434
+ 'ActiveSupport::LogSubscriber.colorize_logging = false if defined?(ActiveSupport::LogSubscriber)' # Disable Rails logging colors
415
435
  ]
416
-
436
+
417
437
  irb_commands.each do |cmd|
418
- begin
419
- @writer.puts cmd
420
- @writer.flush
421
-
422
- # Wait briefly for command to execute
423
- sleep 0.1
424
-
425
- # Clear any output from the configuration command (these commands typically don't produce visible output)
426
- clear_buffer
427
- rescue StandardError => e
428
- logger.warn "[ConsoleSupervisor] Failed to configure IRB setting: #{cmd} - #{e.message}"
429
- end
438
+ @writer.puts cmd
439
+ @writer.flush
440
+
441
+ # Wait briefly for command to execute
442
+ sleep 0.1
443
+
444
+ # Clear any output from the configuration command (these commands typically don't produce visible output)
445
+ clear_buffer
446
+ rescue StandardError => e
447
+ logger.warn "[ConsoleSupervisor] Failed to configure IRB setting: #{cmd} - #{e.message}"
430
448
  end
431
-
449
+
432
450
  # Send multiple empty lines to ensure all settings are processed
433
451
  # This is especially important for remote consoles like kamal console
434
452
  2.times do
@@ -436,121 +454,143 @@ module Consolle
436
454
  @writer.flush
437
455
  sleep 0.05
438
456
  end
439
-
457
+
440
458
  # Clear buffer again after sending empty lines
441
459
  clear_buffer
442
-
460
+
443
461
  # Wait for prompt after configuration with reasonable timeout
444
462
  begin
445
463
  wait_for_prompt(timeout: 2, consume_all: false)
446
464
  rescue Timeout::Error
447
465
  # This can fail with some console types, but that's okay
448
- logger.debug "[ConsoleSupervisor] No prompt after IRB configuration, continuing"
466
+ logger.debug '[ConsoleSupervisor] No prompt after IRB configuration, continuing'
449
467
  end
450
-
468
+
451
469
  # Final buffer clear
452
470
  clear_buffer
453
-
454
- logger.debug "[ConsoleSupervisor] IRB configured for automation"
471
+
472
+ logger.debug '[ConsoleSupervisor] IRB configured for automation'
455
473
  end
456
474
 
457
475
  def strip_ansi(text)
458
476
  # Remove all ANSI escape sequences
459
477
  text
460
- .gsub(/\e\[[\d;]*[a-zA-Z]/, "") # Standard ANSI codes
461
- .gsub(/\e\[\?[\d]+[hl]/, "") # Private mode codes like [?2004h
462
- .gsub(/\e[<>=]/, "") # Other escape sequences
463
- .gsub(/[\x00-\x08\x0B-\x0C\x0E-\x1D\x7F]/, "") # Control chars except \t(09) \n(0A) \r(0D) \u001E(1E) \u001F(1F)
464
- .gsub(/\r\n/, "\n") # Normalize line endings
478
+ .gsub(/\e\[[\d;]*[a-zA-Z]/, '') # Standard ANSI codes
479
+ .gsub(/\e\[\?\d+[hl]/, '') # Private mode codes like [?2004h
480
+ .gsub(/\e[<>=]/, '') # Other escape sequences
481
+ .gsub(/[\x00-\x08\x0B-\x0C\x0E-\x1D\x7F]/, '') # Control chars except \t(09) \n(0A) \r(0D) \u001E(1E) \u001F(1F)
482
+ .gsub(/\r\n/, "\n") # Normalize line endings
465
483
  end
466
-
467
484
 
468
- def parse_output(output, code)
485
+ def parse_output(output, _code)
469
486
  # Remove ANSI codes
470
487
  clean = strip_ansi(output)
471
-
488
+
472
489
  # Split into lines
473
490
  lines = clean.lines
474
491
  result_lines = []
475
492
  skip_echo = true
476
-
493
+
477
494
  lines.each_with_index do |line, idx|
478
495
  # Skip the eval command echo (both file-based and Base64)
479
- if skip_echo && (line.include?("eval(File.read") || line.include?("eval(Base64.decode64"))
496
+ if skip_echo && (line.include?('eval(File.read') || line.include?('eval(Base64.decode64'))
480
497
  skip_echo = false
481
498
  next
482
499
  end
483
-
500
+
484
501
  # Skip prompts (but not return values that start with =>)
485
- if line.match?(PROMPT_PATTERN) && !line.start_with?("=>")
486
- next
487
- end
488
-
502
+ next if line.match?(PROMPT_PATTERN) && !line.start_with?('=>')
503
+
489
504
  # Skip common IRB configuration output patterns
490
505
  if line.match?(/^(IRB\.conf|DISABLE_PRY_RAILS|Switch to inspect mode|Loading .*\.rb|nil)$/) ||
491
506
  line.match?(/^__consolle_init_[\d.]+__$/) ||
492
507
  line.match?(/^'consolle_init'$/) ||
493
- line.strip == "false" && idx == 0 # Skip leading false from IRB config
508
+ line.strip == 'false' && idx == 0 # Skip leading false from IRB config
494
509
  next
495
510
  end
496
-
511
+
497
512
  # Collect all other lines (including return values and side effects)
498
513
  result_lines << line
499
514
  end
500
-
515
+
501
516
  # Join all lines - this includes both side effects and return values
502
- result = result_lines.join.strip
503
-
504
- result
517
+ result_lines.join.strip
505
518
  end
506
519
 
507
520
  def trim_restart_history
508
521
  # Keep only restarts within the window
509
522
  cutoff = Time.now - RESTART_WINDOW
510
523
  @restart_timestamps.keep_if { |t| t > cutoff }
511
-
524
+
512
525
  # Check if too many restarts
513
- if @restart_timestamps.size > MAX_RESTARTS
514
- logger.error "[ConsoleSupervisor] Too many restarts (#{@restart_timestamps.size} in #{RESTART_WINDOW}s)"
515
- # TODO: Send alert to ops team
516
- end
526
+ return unless @restart_timestamps.size > MAX_RESTARTS
527
+
528
+ logger.error "[ConsoleSupervisor] Too many restarts (#{@restart_timestamps.size} in #{RESTART_WINDOW}s)"
529
+ # TODO: Send alert to ops team
517
530
  end
518
531
 
519
532
  def stop_console
520
533
  return unless running?
521
-
534
+
522
535
  begin
523
- @writer.puts("exit")
536
+ @writer.puts('exit')
524
537
  @writer.flush
525
538
  rescue StandardError
526
539
  # PTY might be closed already
527
540
  end
528
-
541
+
529
542
  # Wait for process to exit gracefully
530
- waited = Process.waitpid(@pid, Process::WNOHANG) rescue nil
543
+ waited = begin
544
+ Process.waitpid(@pid, Process::WNOHANG)
545
+ rescue StandardError
546
+ nil
547
+ end
531
548
  unless waited
532
549
  # Give it up to 3 seconds to exit gracefully
533
550
  30.times do
534
551
  sleep 0.1
535
552
  break unless running?
536
- waited = Process.waitpid(@pid, Process::WNOHANG) rescue nil
553
+
554
+ waited = begin
555
+ Process.waitpid(@pid, Process::WNOHANG)
556
+ rescue StandardError
557
+ nil
558
+ end
537
559
  break if waited
538
560
  end
539
561
  end
540
-
562
+
541
563
  # Force kill if still running
542
564
  if running? && !waited
543
- Process.kill("TERM", @pid) rescue nil
565
+ begin
566
+ Process.kill('TERM', @pid)
567
+ rescue StandardError
568
+ nil
569
+ end
544
570
  sleep 0.5
545
- Process.kill("KILL", @pid) rescue nil if running?
571
+ if running?
572
+ begin
573
+ Process.kill('KILL', @pid)
574
+ rescue StandardError
575
+ nil
576
+ end
577
+ end
546
578
  end
547
579
  rescue Errno::ECHILD
548
580
  # Process already gone
549
581
  ensure
550
582
  # Close PTY file descriptors
551
- @reader&.close rescue nil
552
- @writer&.close rescue nil
583
+ begin
584
+ @reader&.close
585
+ rescue StandardError
586
+ nil
587
+ end
588
+ begin
589
+ @writer&.close
590
+ rescue StandardError
591
+ nil
592
+ end
553
593
  end
554
594
  end
555
595
  end
556
- end
596
+ end