consolle 0.2.8 → 0.3.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 +4 -4
- data/.version +1 -1
- data/Gemfile.lock +1 -1
- data/lib/consolle/adapters/rails_console.rb +116 -10
- data/lib/consolle/cli.rb +22 -5
- data/lib/consolle/constants.rb +6 -0
- data/lib/consolle/errors.rb +118 -0
- data/lib/consolle/server/console_socket_server.rb +4 -2
- data/lib/consolle/server/console_supervisor.rb +94 -21
- data/lib/consolle/server/request_broker.rb +9 -2
- data/lib/consolle.rb +2 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a3baea2a10a98486b313276d58a6bdf7bffcf8418b490c004f756e480f38060
|
|
4
|
+
data.tar.gz: 9385cca18d1b3caa4de57c2efb98c17da15ff7cf9520f35deaf9eb0ebd21aeab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7adccc6ed3b0d47cfb80058c6f45eb82454dbe064c57a778f6bf642ddbd6755bca99c93bd460057e25ee0e5347fa006fbf63dfb723e6e2cb6c6b9b23c3fcd68
|
|
7
|
+
data.tar.gz: 99d6ac3e933f46de68891d4d48b05e590e6bd044902f00dc322d468ff136bbe2a839f614da5bf1958e8fb3173df5d5e69f535229aff2ba4cdd1d53beb87d76bf
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.3.0
|
data/Gemfile.lock
CHANGED
|
@@ -5,6 +5,7 @@ require 'json'
|
|
|
5
5
|
require 'timeout'
|
|
6
6
|
require 'securerandom'
|
|
7
7
|
require 'fileutils'
|
|
8
|
+
require_relative '../constants'
|
|
8
9
|
|
|
9
10
|
module Consolle
|
|
10
11
|
module Adapters
|
|
@@ -12,7 +13,7 @@ module Consolle
|
|
|
12
13
|
attr_reader :socket_path, :process_pid, :pid_path, :log_path
|
|
13
14
|
|
|
14
15
|
def initialize(socket_path: nil, pid_path: nil, log_path: nil, rails_root: nil, rails_env: nil, verbose: false,
|
|
15
|
-
command: nil)
|
|
16
|
+
command: nil, wait_timeout: nil)
|
|
16
17
|
@socket_path = socket_path || default_socket_path
|
|
17
18
|
@pid_path = pid_path || default_pid_path
|
|
18
19
|
@log_path = log_path || default_log_path
|
|
@@ -20,6 +21,7 @@ module Consolle
|
|
|
20
21
|
@rails_env = rails_env || 'development'
|
|
21
22
|
@verbose = verbose
|
|
22
23
|
@command = command || 'bin/rails console'
|
|
24
|
+
@wait_timeout = wait_timeout || Consolle::DEFAULT_WAIT_TIMEOUT
|
|
23
25
|
@server_pid = nil
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -30,7 +32,7 @@ module Consolle
|
|
|
30
32
|
start_server_daemon
|
|
31
33
|
|
|
32
34
|
# Wait for server to be ready
|
|
33
|
-
wait_for_server(timeout:
|
|
35
|
+
wait_for_server(timeout: @wait_timeout)
|
|
34
36
|
|
|
35
37
|
# Get server status
|
|
36
38
|
status = get_status
|
|
@@ -146,7 +148,8 @@ module Consolle
|
|
|
146
148
|
@verbose ? 'debug' : 'info',
|
|
147
149
|
@pid_path,
|
|
148
150
|
@log_path,
|
|
149
|
-
@command
|
|
151
|
+
@command,
|
|
152
|
+
@wait_timeout.to_s
|
|
150
153
|
]
|
|
151
154
|
end
|
|
152
155
|
|
|
@@ -156,7 +159,8 @@ module Consolle
|
|
|
156
159
|
require 'consolle/server/console_socket_server'
|
|
157
160
|
require 'logger'
|
|
158
161
|
#{' '}
|
|
159
|
-
socket_path, rails_root, rails_env, log_level, pid_path, log_path, command = ARGV
|
|
162
|
+
socket_path, rails_root, rails_env, log_level, pid_path, log_path, command, wait_timeout_str = ARGV
|
|
163
|
+
wait_timeout = wait_timeout_str ? wait_timeout_str.to_i : nil
|
|
160
164
|
#{' '}
|
|
161
165
|
# Write initial log
|
|
162
166
|
log_file = log_path || socket_path.sub(/\\.socket$/, '.log')
|
|
@@ -186,7 +190,8 @@ module Consolle
|
|
|
186
190
|
rails_root: rails_root,
|
|
187
191
|
rails_env: rails_env,
|
|
188
192
|
logger: logger,
|
|
189
|
-
command: command
|
|
193
|
+
command: command,
|
|
194
|
+
wait_timeout: wait_timeout
|
|
190
195
|
)
|
|
191
196
|
#{' '}
|
|
192
197
|
puts "[Server] Starting server with log level: \#{log_level}..."
|
|
@@ -199,6 +204,17 @@ module Consolle
|
|
|
199
204
|
rescue => e
|
|
200
205
|
puts "[Server] Error: \#{e.class}: \#{e.message}"
|
|
201
206
|
puts e.backtrace.join("\\n")
|
|
207
|
+
|
|
208
|
+
# Clean up socket file if it exists
|
|
209
|
+
if defined?(socket_path) && socket_path && File.exist?(socket_path)
|
|
210
|
+
File.unlink(socket_path) rescue nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Clean up PID file if it exists
|
|
214
|
+
if defined?(pid_file) && pid_file && File.exist?(pid_file)
|
|
215
|
+
File.unlink(pid_file) rescue nil
|
|
216
|
+
end
|
|
217
|
+
|
|
202
218
|
exit 1
|
|
203
219
|
end
|
|
204
220
|
RUBY
|
|
@@ -213,8 +229,8 @@ module Consolle
|
|
|
213
229
|
log_file = @log_path
|
|
214
230
|
pid = Process.spawn(
|
|
215
231
|
*server_command,
|
|
216
|
-
out: log_file,
|
|
217
|
-
err: log_file
|
|
232
|
+
out: [log_file, 'a'],
|
|
233
|
+
err: [log_file, 'a']
|
|
218
234
|
)
|
|
219
235
|
|
|
220
236
|
Process.detach(pid)
|
|
@@ -255,16 +271,106 @@ module Consolle
|
|
|
255
271
|
File.unlink(pid_file) if File.exist?(pid_file)
|
|
256
272
|
end
|
|
257
273
|
|
|
258
|
-
def wait_for_server(timeout:
|
|
274
|
+
def wait_for_server(timeout: Consolle::DEFAULT_WAIT_TIMEOUT)
|
|
259
275
|
deadline = Time.now + timeout
|
|
276
|
+
server_pid = nil
|
|
277
|
+
error_found = false
|
|
278
|
+
error_message = nil
|
|
279
|
+
last_log_check = Time.now
|
|
280
|
+
ssh_auth_detected = false
|
|
281
|
+
|
|
282
|
+
# Record the initial log file position to avoid reading old errors
|
|
283
|
+
initial_log_pos = if File.exist?(@log_path)
|
|
284
|
+
File.size(@log_path)
|
|
285
|
+
else
|
|
286
|
+
0
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
puts "Waiting for console to start (timeout: #{timeout}s)..." if @verbose
|
|
260
290
|
|
|
261
291
|
while Time.now < deadline
|
|
262
|
-
|
|
292
|
+
# Check if server process is still alive by checking pid file
|
|
293
|
+
if File.exist?(@pid_path)
|
|
294
|
+
server_pid ||= File.read(@pid_path).to_i
|
|
295
|
+
begin
|
|
296
|
+
Process.kill(0, server_pid)
|
|
297
|
+
# Process is alive
|
|
298
|
+
rescue Errno::ESRCH
|
|
299
|
+
# Process died - check log for error
|
|
300
|
+
if File.exist?(@log_path)
|
|
301
|
+
File.open(@log_path, 'r') do |f|
|
|
302
|
+
f.seek(initial_log_pos)
|
|
303
|
+
log_content = f.read
|
|
304
|
+
if log_content.include?('[Server] Error:')
|
|
305
|
+
error_lines = log_content.lines.grep(/\[Server\] Error:/)
|
|
306
|
+
error_message = error_lines.last.strip if error_lines.any?
|
|
307
|
+
else
|
|
308
|
+
error_message = "Server process died unexpectedly"
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
else
|
|
312
|
+
error_message = "Server process died unexpectedly"
|
|
313
|
+
end
|
|
314
|
+
error_found = true
|
|
315
|
+
break
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Check log file periodically for errors or SSH auth messages
|
|
320
|
+
if Time.now - last_log_check > 0.5
|
|
321
|
+
last_log_check = Time.now
|
|
322
|
+
if File.exist?(@log_path) && File.size(@log_path) > initial_log_pos
|
|
323
|
+
File.open(@log_path, 'r') do |f|
|
|
324
|
+
f.seek(initial_log_pos)
|
|
325
|
+
log_content = f.read
|
|
326
|
+
|
|
327
|
+
# Check for explicit errors
|
|
328
|
+
if log_content.include?('[Server] Error:')
|
|
329
|
+
error_lines = log_content.lines.grep(/\[Server\] Error:/)
|
|
330
|
+
error_message = error_lines.last.strip if error_lines.any?
|
|
331
|
+
error_found = true
|
|
332
|
+
break
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Check for SSH authentication messages
|
|
336
|
+
if !ssh_auth_detected && (log_content.include?('SSH') ||
|
|
337
|
+
log_content.include?('ssh') ||
|
|
338
|
+
log_content.include?('Authenticating') ||
|
|
339
|
+
log_content.include?('authentication') ||
|
|
340
|
+
log_content.include?('1Password') ||
|
|
341
|
+
@command.include?('kamal') ||
|
|
342
|
+
@command.include?('ssh'))
|
|
343
|
+
ssh_auth_detected = true
|
|
344
|
+
puts "SSH authentication detected, extending timeout..." if @verbose
|
|
345
|
+
# Extend deadline for SSH auth
|
|
346
|
+
deadline = Time.now + [timeout, 60].max
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Check if socket exists and server is responsive
|
|
353
|
+
if File.exist?(@socket_path)
|
|
354
|
+
begin
|
|
355
|
+
status = get_status
|
|
356
|
+
if status && status['success'] && status['running']
|
|
357
|
+
return true
|
|
358
|
+
end
|
|
359
|
+
rescue StandardError
|
|
360
|
+
# Socket exists but not ready yet, continue waiting
|
|
361
|
+
end
|
|
362
|
+
end
|
|
263
363
|
|
|
264
364
|
sleep 0.1
|
|
265
365
|
end
|
|
266
366
|
|
|
267
|
-
|
|
367
|
+
if error_found
|
|
368
|
+
raise "Server failed to start: #{error_message || 'Unknown error'}"
|
|
369
|
+
else
|
|
370
|
+
timeout_msg = "Server failed to start within #{timeout} seconds"
|
|
371
|
+
timeout_msg += " (SSH authentication may be required)" if ssh_auth_detected || @command.include?('ssh') || @command.include?('kamal')
|
|
372
|
+
raise timeout_msg
|
|
373
|
+
end
|
|
268
374
|
end
|
|
269
375
|
|
|
270
376
|
def send_request(request, timeout: 30)
|
data/lib/consolle/cli.rb
CHANGED
|
@@ -7,6 +7,7 @@ require 'socket'
|
|
|
7
7
|
require 'timeout'
|
|
8
8
|
require 'securerandom'
|
|
9
9
|
require 'date'
|
|
10
|
+
require_relative 'constants'
|
|
10
11
|
require_relative 'adapters/rails_console'
|
|
11
12
|
|
|
12
13
|
module Consolle
|
|
@@ -126,9 +127,13 @@ module Consolle
|
|
|
126
127
|
Custom console commands are supported for special environments:
|
|
127
128
|
cone start --command "kamal app exec -i 'bin/rails console'"
|
|
128
129
|
cone start --command "docker exec -it myapp bin/rails console"
|
|
130
|
+
|
|
131
|
+
For SSH-based commands that require authentication (e.g., 1Password SSH agent):
|
|
132
|
+
cone start --command "kamal console" --wait-timeout 60
|
|
129
133
|
LONGDESC
|
|
130
134
|
method_option :rails_env, type: :string, aliases: '-e', desc: 'Rails environment', default: 'development'
|
|
131
135
|
method_option :command, type: :string, aliases: '-c', desc: 'Custom console command', default: 'bin/rails console'
|
|
136
|
+
method_option :wait_timeout, type: :numeric, aliases: '-w', desc: 'Timeout for console startup (seconds)', default: Consolle::DEFAULT_WAIT_TIMEOUT
|
|
132
137
|
def start
|
|
133
138
|
ensure_rails_project!
|
|
134
139
|
ensure_project_directories
|
|
@@ -155,7 +160,7 @@ module Consolle
|
|
|
155
160
|
clear_session_info
|
|
156
161
|
end
|
|
157
162
|
|
|
158
|
-
adapter = create_rails_adapter(options[:rails_env], options[:target], options[:command])
|
|
163
|
+
adapter = create_rails_adapter(options[:rails_env], options[:target], options[:command], options[:wait_timeout])
|
|
159
164
|
|
|
160
165
|
puts 'Starting Rails console...'
|
|
161
166
|
|
|
@@ -553,9 +558,20 @@ module Consolle
|
|
|
553
558
|
puts result['result'] unless result['result'].nil?
|
|
554
559
|
puts "Execution time: #{result['execution_time']}s" if options[:verbose] && result['execution_time']
|
|
555
560
|
else
|
|
556
|
-
|
|
561
|
+
# Display error information
|
|
562
|
+
if result['error_code']
|
|
563
|
+
puts "Error: #{result['error_code']}"
|
|
564
|
+
else
|
|
565
|
+
puts "Error: #{result['error']}"
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# Show error class in verbose mode
|
|
569
|
+
if options[:verbose] && result['error_class']
|
|
570
|
+
puts "Error Class: #{result['error_class']}"
|
|
571
|
+
end
|
|
572
|
+
|
|
557
573
|
puts result['message']
|
|
558
|
-
puts result['backtrace']&.join("\n") if options[:verbose]
|
|
574
|
+
puts result['backtrace']&.join("\n") if options[:verbose] && result['backtrace']
|
|
559
575
|
exit 1
|
|
560
576
|
end
|
|
561
577
|
end
|
|
@@ -633,7 +649,7 @@ module Consolle
|
|
|
633
649
|
File.join(Dir.pwd, 'tmp', 'cone', 'sessions.json')
|
|
634
650
|
end
|
|
635
651
|
|
|
636
|
-
def create_rails_adapter(rails_env = 'development', target = nil, command = nil)
|
|
652
|
+
def create_rails_adapter(rails_env = 'development', target = nil, command = nil, wait_timeout = nil)
|
|
637
653
|
target ||= options[:target]
|
|
638
654
|
|
|
639
655
|
Consolle::Adapters::RailsConsole.new(
|
|
@@ -643,7 +659,8 @@ module Consolle
|
|
|
643
659
|
rails_root: Dir.pwd,
|
|
644
660
|
rails_env: rails_env,
|
|
645
661
|
verbose: options[:verbose],
|
|
646
|
-
command: command
|
|
662
|
+
command: command,
|
|
663
|
+
wait_timeout: wait_timeout
|
|
647
664
|
)
|
|
648
665
|
end
|
|
649
666
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Consolle
|
|
4
|
+
module Errors
|
|
5
|
+
# Base error class for all Consolle errors
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Timeout errors hierarchy
|
|
9
|
+
class Timeout < Error; end
|
|
10
|
+
|
|
11
|
+
# Socket communication timeout (CLI/Adapter layer)
|
|
12
|
+
class SocketTimeout < Timeout
|
|
13
|
+
def initialize(timeout_seconds)
|
|
14
|
+
super("Socket operation timed out after #{timeout_seconds} seconds")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Request processing timeout (Broker layer)
|
|
19
|
+
class RequestTimeout < Timeout
|
|
20
|
+
def initialize
|
|
21
|
+
super("Request processing timed out")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Code execution timeout (Supervisor layer)
|
|
26
|
+
class ExecutionTimeout < Timeout
|
|
27
|
+
def initialize(timeout_seconds)
|
|
28
|
+
super("Code execution timed out after #{timeout_seconds} seconds")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Execution errors
|
|
33
|
+
class ExecutionError < Error; end
|
|
34
|
+
|
|
35
|
+
# Syntax error in executed code
|
|
36
|
+
class SyntaxError < ExecutionError
|
|
37
|
+
def initialize(message)
|
|
38
|
+
super("Syntax error: #{message}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Runtime error in executed code
|
|
43
|
+
class RuntimeError < ExecutionError
|
|
44
|
+
def initialize(message)
|
|
45
|
+
super("Runtime error: #{message}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Load error (missing gem, file, etc.)
|
|
50
|
+
class LoadError < ExecutionError
|
|
51
|
+
def initialize(message)
|
|
52
|
+
super("Load error: #{message}")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Error classifier to map exceptions to error codes
|
|
57
|
+
class ErrorClassifier
|
|
58
|
+
ERROR_CODE_MAP = {
|
|
59
|
+
'Timeout::Error' => 'EXECUTION_TIMEOUT',
|
|
60
|
+
'Consolle::Errors::SocketTimeout' => 'SOCKET_TIMEOUT',
|
|
61
|
+
'Consolle::Errors::RequestTimeout' => 'REQUEST_TIMEOUT',
|
|
62
|
+
'Consolle::Errors::ExecutionTimeout' => 'EXECUTION_TIMEOUT',
|
|
63
|
+
'SyntaxError' => 'SYNTAX_ERROR',
|
|
64
|
+
'::SyntaxError' => 'SYNTAX_ERROR',
|
|
65
|
+
'LoadError' => 'LOAD_ERROR',
|
|
66
|
+
'::LoadError' => 'LOAD_ERROR',
|
|
67
|
+
'NameError' => 'NAME_ERROR',
|
|
68
|
+
'NoMethodError' => 'NO_METHOD_ERROR',
|
|
69
|
+
'ArgumentError' => 'ARGUMENT_ERROR',
|
|
70
|
+
'TypeError' => 'TYPE_ERROR',
|
|
71
|
+
'ZeroDivisionError' => 'ZERO_DIVISION_ERROR',
|
|
72
|
+
'RuntimeError' => 'RUNTIME_ERROR',
|
|
73
|
+
'::RuntimeError' => 'RUNTIME_ERROR',
|
|
74
|
+
'StandardError' => 'STANDARD_ERROR',
|
|
75
|
+
'Exception' => 'EXCEPTION'
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
def self.to_code(exception)
|
|
79
|
+
return 'UNKNOWN_ERROR' unless exception.is_a?(Exception)
|
|
80
|
+
|
|
81
|
+
# Try exact class match first
|
|
82
|
+
error_code = ERROR_CODE_MAP[exception.class.name]
|
|
83
|
+
return error_code if error_code
|
|
84
|
+
|
|
85
|
+
# Try with leading :: for core Ruby errors
|
|
86
|
+
error_code = ERROR_CODE_MAP["::#{exception.class.name}"]
|
|
87
|
+
return error_code if error_code
|
|
88
|
+
|
|
89
|
+
# Check inheritance chain
|
|
90
|
+
exception.class.ancestors.each do |klass|
|
|
91
|
+
error_code = ERROR_CODE_MAP[klass.name]
|
|
92
|
+
return error_code if error_code
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
'UNKNOWN_ERROR'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.classify_message(error_message)
|
|
99
|
+
case error_message
|
|
100
|
+
when /syntax error/i
|
|
101
|
+
'SYNTAX_ERROR'
|
|
102
|
+
when /cannot load such file|no such file to load/i
|
|
103
|
+
'LOAD_ERROR'
|
|
104
|
+
when /undefined local variable or method/i, /undefined method/i
|
|
105
|
+
'NAME_ERROR'
|
|
106
|
+
when /wrong number of arguments/i
|
|
107
|
+
'ARGUMENT_ERROR'
|
|
108
|
+
when /execution timed out/i
|
|
109
|
+
'EXECUTION_TIMEOUT'
|
|
110
|
+
when /request timed out/i
|
|
111
|
+
'REQUEST_TIMEOUT'
|
|
112
|
+
else
|
|
113
|
+
'EXECUTION_ERROR'
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -12,11 +12,12 @@ module Consolle
|
|
|
12
12
|
class ConsoleSocketServer
|
|
13
13
|
attr_reader :socket_path, :logger
|
|
14
14
|
|
|
15
|
-
def initialize(socket_path:, rails_root:, rails_env: 'development', logger: nil, command: nil)
|
|
15
|
+
def initialize(socket_path:, rails_root:, rails_env: 'development', logger: nil, command: nil, wait_timeout: nil)
|
|
16
16
|
@socket_path = socket_path
|
|
17
17
|
@rails_root = rails_root
|
|
18
18
|
@rails_env = rails_env
|
|
19
19
|
@command = command || 'bin/rails console'
|
|
20
|
+
@wait_timeout = wait_timeout
|
|
20
21
|
@logger = logger || begin
|
|
21
22
|
log = Logger.new(STDOUT)
|
|
22
23
|
log.level = Logger::DEBUG
|
|
@@ -100,7 +101,8 @@ module Consolle
|
|
|
100
101
|
rails_root: @rails_root,
|
|
101
102
|
rails_env: @rails_env,
|
|
102
103
|
logger: @logger,
|
|
103
|
-
command: @command
|
|
104
|
+
command: @command,
|
|
105
|
+
wait_timeout: @wait_timeout
|
|
104
106
|
)
|
|
105
107
|
end
|
|
106
108
|
|
|
@@ -4,6 +4,8 @@ require 'pty'
|
|
|
4
4
|
require 'timeout'
|
|
5
5
|
require 'fcntl'
|
|
6
6
|
require 'logger'
|
|
7
|
+
require_relative '../constants'
|
|
8
|
+
require_relative '../errors'
|
|
7
9
|
|
|
8
10
|
# Ruby 3.4.0+ extracts base64 as a default gem
|
|
9
11
|
# Suppress warning by silencing verbose mode temporarily
|
|
@@ -26,11 +28,12 @@ module Consolle
|
|
|
26
28
|
PROMPT_PATTERN = /^[^\w]*(\u001E\u001F<CONSOLLE>\u001F\u001E|\w+[-_]?\w*\([^)]*\)>|irb\([^)]+\):\d+:?\d*[>*]|>>|>)\s*$/
|
|
27
29
|
CTRL_C = "\x03"
|
|
28
30
|
|
|
29
|
-
def initialize(rails_root:, rails_env: 'development', logger: nil, command: nil)
|
|
31
|
+
def initialize(rails_root:, rails_env: 'development', logger: nil, command: nil, wait_timeout: nil)
|
|
30
32
|
@rails_root = rails_root
|
|
31
33
|
@rails_env = rails_env
|
|
32
34
|
@command = command || 'bin/rails console'
|
|
33
35
|
@logger = logger || Logger.new(STDOUT)
|
|
36
|
+
@wait_timeout = wait_timeout || Consolle::DEFAULT_WAIT_TIMEOUT
|
|
34
37
|
@pid = nil
|
|
35
38
|
@reader = nil
|
|
36
39
|
@writer = nil
|
|
@@ -94,9 +97,9 @@ module Consolle
|
|
|
94
97
|
end
|
|
95
98
|
encoded_code = Base64.strict_encode64(utf8_code)
|
|
96
99
|
|
|
97
|
-
# Use eval to execute the Base64-decoded code
|
|
100
|
+
# Use eval to execute the Base64-decoded code with exception handling
|
|
98
101
|
# 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)"
|
|
102
|
+
eval_command = "begin; eval(Base64.decode64('#{encoded_code}').force_encoding('UTF-8'), IRB.CurrentContext.workspace.binding); rescue Exception => e; puts \"\#{e.class}: \#{e.message}\"; nil; end"
|
|
100
103
|
logger.debug '[ConsoleSupervisor] Sending eval command (Base64 encoded)'
|
|
101
104
|
@writer.puts eval_command
|
|
102
105
|
@writer.flush
|
|
@@ -113,11 +116,7 @@ module Consolle
|
|
|
113
116
|
@writer.flush
|
|
114
117
|
sleep 0.5
|
|
115
118
|
clear_buffer
|
|
116
|
-
return
|
|
117
|
-
success: false,
|
|
118
|
-
output: "Execution timed out after #{timeout} seconds",
|
|
119
|
-
execution_time: timeout
|
|
120
|
-
}
|
|
119
|
+
return build_timeout_response(timeout)
|
|
121
120
|
end
|
|
122
121
|
|
|
123
122
|
begin
|
|
@@ -142,25 +141,29 @@ module Consolle
|
|
|
142
141
|
# PTY can throw EIO when no data available
|
|
143
142
|
IO.select([@reader], nil, nil, 0.1)
|
|
144
143
|
rescue EOFError
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
output: 'Console terminated',
|
|
144
|
+
return build_error_response(
|
|
145
|
+
EOFError.new('Console terminated'),
|
|
148
146
|
execution_time: nil
|
|
149
|
-
|
|
147
|
+
)
|
|
150
148
|
end
|
|
151
149
|
end
|
|
152
150
|
|
|
153
151
|
# Parse and return result
|
|
154
|
-
|
|
152
|
+
parsed_result = parse_output(output, eval_command)
|
|
155
153
|
|
|
156
154
|
# Log for debugging object output issues
|
|
157
155
|
logger.debug "[ConsoleSupervisor] Raw output: #{output.inspect}"
|
|
158
|
-
logger.debug "[ConsoleSupervisor] Parsed result: #{
|
|
156
|
+
logger.debug "[ConsoleSupervisor] Parsed result: #{parsed_result.inspect}"
|
|
159
157
|
|
|
160
|
-
|
|
158
|
+
# Check if the output contains an error
|
|
159
|
+
if parsed_result.is_a?(Hash) && parsed_result[:error]
|
|
160
|
+
build_error_response(parsed_result[:exception], execution_time: nil)
|
|
161
|
+
else
|
|
162
|
+
{ success: true, output: parsed_result, execution_time: nil }
|
|
163
|
+
end
|
|
161
164
|
rescue StandardError => e
|
|
162
165
|
logger.error "[ConsoleSupervisor] Eval error: #{e.message}"
|
|
163
|
-
|
|
166
|
+
build_error_response(e, execution_time: nil)
|
|
164
167
|
end
|
|
165
168
|
end
|
|
166
169
|
end
|
|
@@ -204,6 +207,39 @@ module Consolle
|
|
|
204
207
|
|
|
205
208
|
private
|
|
206
209
|
|
|
210
|
+
def build_timeout_response(timeout_seconds)
|
|
211
|
+
error = Consolle::Errors::ExecutionTimeout.new(timeout_seconds)
|
|
212
|
+
{
|
|
213
|
+
success: false,
|
|
214
|
+
error_class: error.class.name,
|
|
215
|
+
error_code: Consolle::Errors::ErrorClassifier.to_code(error),
|
|
216
|
+
output: error.message,
|
|
217
|
+
execution_time: timeout_seconds
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def build_error_response(exception, execution_time: nil)
|
|
222
|
+
# Handle string messages that come from eval output parsing
|
|
223
|
+
if exception.is_a?(String)
|
|
224
|
+
error_code = Consolle::Errors::ErrorClassifier.classify_message(exception)
|
|
225
|
+
return {
|
|
226
|
+
success: false,
|
|
227
|
+
error_class: 'RuntimeError',
|
|
228
|
+
error_code: error_code,
|
|
229
|
+
output: exception,
|
|
230
|
+
execution_time: execution_time
|
|
231
|
+
}
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
{
|
|
235
|
+
success: false,
|
|
236
|
+
error_class: exception.class.name,
|
|
237
|
+
error_code: Consolle::Errors::ErrorClassifier.to_code(exception),
|
|
238
|
+
output: "#{exception.class}: #{exception.message}",
|
|
239
|
+
execution_time: execution_time
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
207
243
|
def spawn_console
|
|
208
244
|
env = {
|
|
209
245
|
'RAILS_ENV' => @rails_env,
|
|
@@ -246,7 +282,7 @@ module Consolle
|
|
|
246
282
|
trim_restart_history
|
|
247
283
|
|
|
248
284
|
# Wait for initial prompt
|
|
249
|
-
wait_for_prompt(timeout:
|
|
285
|
+
wait_for_prompt(timeout: @wait_timeout)
|
|
250
286
|
|
|
251
287
|
# Configure IRB settings for automation
|
|
252
288
|
configure_irb_for_automation
|
|
@@ -352,12 +388,17 @@ module Consolle
|
|
|
352
388
|
|
|
353
389
|
def wait_for_prompt(timeout: 15, consume_all: false)
|
|
354
390
|
output = +''
|
|
355
|
-
|
|
391
|
+
start_time = Time.now
|
|
392
|
+
deadline = start_time + timeout
|
|
356
393
|
prompt_found = false
|
|
357
394
|
last_data_time = Time.now
|
|
358
395
|
|
|
396
|
+
logger.info "[ConsoleSupervisor] Waiting for prompt with timeout: #{timeout}s (deadline: #{deadline}, now: #{start_time})"
|
|
397
|
+
|
|
359
398
|
loop do
|
|
360
|
-
|
|
399
|
+
current_time = Time.now
|
|
400
|
+
if current_time > deadline
|
|
401
|
+
logger.error "[ConsoleSupervisor] Timeout reached. Current: #{current_time}, Deadline: #{deadline}, Elapsed: #{current_time - start_time}s"
|
|
361
402
|
logger.error "[ConsoleSupervisor] Output so far: #{output.inspect}"
|
|
362
403
|
logger.error "[ConsoleSupervisor] Stripped: #{strip_ansi(output).inspect}"
|
|
363
404
|
raise Timeout::Error, "No prompt after #{timeout} seconds"
|
|
@@ -490,6 +531,7 @@ module Consolle
|
|
|
490
531
|
lines = clean.lines
|
|
491
532
|
result_lines = []
|
|
492
533
|
skip_echo = true
|
|
534
|
+
error_exception = nil
|
|
493
535
|
|
|
494
536
|
lines.each_with_index do |line, idx|
|
|
495
537
|
# Skip the eval command echo (both file-based and Base64)
|
|
@@ -509,12 +551,43 @@ module Consolle
|
|
|
509
551
|
next
|
|
510
552
|
end
|
|
511
553
|
|
|
554
|
+
# Check for error patterns
|
|
555
|
+
if !error_exception && line.match?(/^(.*Error|.*Exception):/)
|
|
556
|
+
error_match = line.match(/^((?:\w+::)*\w*(?:Error|Exception)):\s*(.*)/)
|
|
557
|
+
if error_match
|
|
558
|
+
error_class = error_match[1]
|
|
559
|
+
error_message = error_match[2]
|
|
560
|
+
|
|
561
|
+
# Try to create the actual exception object
|
|
562
|
+
begin
|
|
563
|
+
# Handle namespaced errors
|
|
564
|
+
if error_class.include?('::')
|
|
565
|
+
parts = error_class.split('::')
|
|
566
|
+
klass = Object
|
|
567
|
+
parts.each { |part| klass = klass.const_get(part) }
|
|
568
|
+
error_exception = klass.new(error_message)
|
|
569
|
+
else
|
|
570
|
+
# Try core Ruby error classes
|
|
571
|
+
error_exception = Object.const_get(error_class).new(error_message)
|
|
572
|
+
end
|
|
573
|
+
rescue NameError
|
|
574
|
+
# If we can't find the exact error class, use a generic RuntimeError
|
|
575
|
+
error_exception = RuntimeError.new("#{error_class}: #{error_message}")
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
512
580
|
# Collect all other lines (including return values and side effects)
|
|
513
581
|
result_lines << line
|
|
514
582
|
end
|
|
515
583
|
|
|
516
|
-
#
|
|
517
|
-
|
|
584
|
+
# If an error was detected, return it as a hash
|
|
585
|
+
if error_exception
|
|
586
|
+
{ error: true, exception: error_exception, output: result_lines.join.strip }
|
|
587
|
+
else
|
|
588
|
+
# Join all lines - this includes both side effects and return values
|
|
589
|
+
result_lines.join.strip
|
|
590
|
+
end
|
|
518
591
|
end
|
|
519
592
|
|
|
520
593
|
def trim_restart_history
|
|
@@ -172,11 +172,18 @@ module Consolle
|
|
|
172
172
|
'execution_time' => result[:execution_time]
|
|
173
173
|
}
|
|
174
174
|
else
|
|
175
|
-
{
|
|
175
|
+
response = {
|
|
176
176
|
'success' => false,
|
|
177
|
-
'error' => 'ExecutionError',
|
|
177
|
+
'error' => result[:error_code] || 'ExecutionError', # Use error_code for backward compatibility
|
|
178
178
|
'message' => result[:output]
|
|
179
179
|
}
|
|
180
|
+
|
|
181
|
+
# Add new fields if present
|
|
182
|
+
response['error_class'] = result[:error_class] if result[:error_class]
|
|
183
|
+
response['error_code'] = result[:error_code] if result[:error_code]
|
|
184
|
+
response['execution_time'] = result[:execution_time] if result[:execution_time]
|
|
185
|
+
|
|
186
|
+
response
|
|
180
187
|
end
|
|
181
188
|
end
|
|
182
189
|
|
data/lib/consolle.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'consolle/version'
|
|
4
|
+
require_relative 'consolle/constants'
|
|
5
|
+
require_relative 'consolle/errors'
|
|
4
6
|
require_relative 'consolle/cli'
|
|
5
7
|
|
|
6
8
|
# Server components
|
|
@@ -9,5 +11,4 @@ require_relative 'consolle/server/console_supervisor'
|
|
|
9
11
|
require_relative 'consolle/server/request_broker'
|
|
10
12
|
|
|
11
13
|
module Consolle
|
|
12
|
-
class Error < StandardError; end
|
|
13
14
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: consolle
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- nacyot
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-07-
|
|
10
|
+
date: 2025-07-26 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: logger
|
|
@@ -79,6 +79,8 @@ files:
|
|
|
79
79
|
- lib/consolle.rb
|
|
80
80
|
- lib/consolle/adapters/rails_console.rb
|
|
81
81
|
- lib/consolle/cli.rb
|
|
82
|
+
- lib/consolle/constants.rb
|
|
83
|
+
- lib/consolle/errors.rb
|
|
82
84
|
- lib/consolle/server/console_socket_server.rb
|
|
83
85
|
- lib/consolle/server/console_supervisor.rb
|
|
84
86
|
- lib/consolle/server/request_broker.rb
|