consolle 0.2.7 → 0.2.9
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 +102 -10
- data/lib/consolle/cli.rb +21 -5
- data/lib/consolle/errors.rb +118 -0
- data/lib/consolle/server/console_socket_server.rb +4 -2
- data/lib/consolle/server/console_supervisor.rb +107 -20
- data/lib/consolle/server/request_broker.rb +9 -2
- data/lib/consolle.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 826cb825a86940fa018abc635b0c7b5914999d85da2c079801d7b98a85166adb
|
|
4
|
+
data.tar.gz: 919448817dd2064b93214be249eeb840bb328ef3c907ab9f8456daf2a8d45992
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3fb3a9902a388e4fe3d551e867c4d6ceec83aff1087e54a48f972c5bb1e029b9216ca4baa5a7c83d1eeee9905285607093cb09e790eda60583f7a66efa501d3
|
|
7
|
+
data.tar.gz: 4a5de0ff45adc32c57a35595b6743133a7f4d5cf1911d0c6dba50c5cd1ae11b0779942845af3a69d8d01c6ddb77745a81d106dfa2639b3e41ee34c91c593fcf2
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.9
|
data/Gemfile.lock
CHANGED
|
@@ -12,7 +12,7 @@ module Consolle
|
|
|
12
12
|
attr_reader :socket_path, :process_pid, :pid_path, :log_path
|
|
13
13
|
|
|
14
14
|
def initialize(socket_path: nil, pid_path: nil, log_path: nil, rails_root: nil, rails_env: nil, verbose: false,
|
|
15
|
-
command: nil)
|
|
15
|
+
command: nil, wait_timeout: nil)
|
|
16
16
|
@socket_path = socket_path || default_socket_path
|
|
17
17
|
@pid_path = pid_path || default_pid_path
|
|
18
18
|
@log_path = log_path || default_log_path
|
|
@@ -20,6 +20,7 @@ module Consolle
|
|
|
20
20
|
@rails_env = rails_env || 'development'
|
|
21
21
|
@verbose = verbose
|
|
22
22
|
@command = command || 'bin/rails console'
|
|
23
|
+
@wait_timeout = wait_timeout || 15
|
|
23
24
|
@server_pid = nil
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -30,7 +31,7 @@ module Consolle
|
|
|
30
31
|
start_server_daemon
|
|
31
32
|
|
|
32
33
|
# Wait for server to be ready
|
|
33
|
-
wait_for_server(timeout:
|
|
34
|
+
wait_for_server(timeout: @wait_timeout)
|
|
34
35
|
|
|
35
36
|
# Get server status
|
|
36
37
|
status = get_status
|
|
@@ -146,7 +147,8 @@ module Consolle
|
|
|
146
147
|
@verbose ? 'debug' : 'info',
|
|
147
148
|
@pid_path,
|
|
148
149
|
@log_path,
|
|
149
|
-
@command
|
|
150
|
+
@command,
|
|
151
|
+
@wait_timeout.to_s
|
|
150
152
|
]
|
|
151
153
|
end
|
|
152
154
|
|
|
@@ -156,7 +158,8 @@ module Consolle
|
|
|
156
158
|
require 'consolle/server/console_socket_server'
|
|
157
159
|
require 'logger'
|
|
158
160
|
#{' '}
|
|
159
|
-
socket_path, rails_root, rails_env, log_level, pid_path, log_path, command = ARGV
|
|
161
|
+
socket_path, rails_root, rails_env, log_level, pid_path, log_path, command, wait_timeout_str = ARGV
|
|
162
|
+
wait_timeout = wait_timeout_str ? wait_timeout_str.to_i : nil
|
|
160
163
|
#{' '}
|
|
161
164
|
# Write initial log
|
|
162
165
|
log_file = log_path || socket_path.sub(/\\.socket$/, '.log')
|
|
@@ -186,7 +189,8 @@ module Consolle
|
|
|
186
189
|
rails_root: rails_root,
|
|
187
190
|
rails_env: rails_env,
|
|
188
191
|
logger: logger,
|
|
189
|
-
command: command
|
|
192
|
+
command: command,
|
|
193
|
+
wait_timeout: wait_timeout
|
|
190
194
|
)
|
|
191
195
|
#{' '}
|
|
192
196
|
puts "[Server] Starting server with log level: \#{log_level}..."
|
|
@@ -199,6 +203,17 @@ module Consolle
|
|
|
199
203
|
rescue => e
|
|
200
204
|
puts "[Server] Error: \#{e.class}: \#{e.message}"
|
|
201
205
|
puts e.backtrace.join("\\n")
|
|
206
|
+
|
|
207
|
+
# Clean up socket file if it exists
|
|
208
|
+
if defined?(socket_path) && socket_path && File.exist?(socket_path)
|
|
209
|
+
File.unlink(socket_path) rescue nil
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Clean up PID file if it exists
|
|
213
|
+
if defined?(pid_file) && pid_file && File.exist?(pid_file)
|
|
214
|
+
File.unlink(pid_file) rescue nil
|
|
215
|
+
end
|
|
216
|
+
|
|
202
217
|
exit 1
|
|
203
218
|
end
|
|
204
219
|
RUBY
|
|
@@ -213,8 +228,8 @@ module Consolle
|
|
|
213
228
|
log_file = @log_path
|
|
214
229
|
pid = Process.spawn(
|
|
215
230
|
*server_command,
|
|
216
|
-
out: log_file,
|
|
217
|
-
err: log_file
|
|
231
|
+
out: [log_file, 'a'],
|
|
232
|
+
err: [log_file, 'a']
|
|
218
233
|
)
|
|
219
234
|
|
|
220
235
|
Process.detach(pid)
|
|
@@ -255,16 +270,93 @@ module Consolle
|
|
|
255
270
|
File.unlink(pid_file) if File.exist?(pid_file)
|
|
256
271
|
end
|
|
257
272
|
|
|
258
|
-
def wait_for_server(timeout:
|
|
273
|
+
def wait_for_server(timeout: 15)
|
|
259
274
|
deadline = Time.now + timeout
|
|
275
|
+
server_pid = nil
|
|
276
|
+
error_found = false
|
|
277
|
+
error_message = nil
|
|
278
|
+
last_log_check = Time.now
|
|
279
|
+
ssh_auth_detected = false
|
|
280
|
+
|
|
281
|
+
puts "Waiting for console to start (timeout: #{timeout}s)..." if @verbose
|
|
260
282
|
|
|
261
283
|
while Time.now < deadline
|
|
262
|
-
|
|
284
|
+
# Check if server process is still alive by checking pid file
|
|
285
|
+
if File.exist?(@pid_path)
|
|
286
|
+
server_pid ||= File.read(@pid_path).to_i
|
|
287
|
+
begin
|
|
288
|
+
Process.kill(0, server_pid)
|
|
289
|
+
# Process is alive
|
|
290
|
+
rescue Errno::ESRCH
|
|
291
|
+
# Process died - check log for error
|
|
292
|
+
if File.exist?(@log_path)
|
|
293
|
+
log_content = File.read(@log_path)
|
|
294
|
+
if log_content.include?('[Server] Error:')
|
|
295
|
+
error_lines = log_content.lines.grep(/\[Server\] Error:/)
|
|
296
|
+
error_message = error_lines.last.strip if error_lines.any?
|
|
297
|
+
else
|
|
298
|
+
error_message = "Server process died unexpectedly"
|
|
299
|
+
end
|
|
300
|
+
else
|
|
301
|
+
error_message = "Server process died unexpectedly"
|
|
302
|
+
end
|
|
303
|
+
error_found = true
|
|
304
|
+
break
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Check log file periodically for errors or SSH auth messages
|
|
309
|
+
if Time.now - last_log_check > 0.5
|
|
310
|
+
last_log_check = Time.now
|
|
311
|
+
if File.exist?(@log_path)
|
|
312
|
+
log_content = File.read(@log_path)
|
|
313
|
+
|
|
314
|
+
# Check for explicit errors
|
|
315
|
+
if log_content.include?('[Server] Error:')
|
|
316
|
+
error_lines = log_content.lines.grep(/\[Server\] Error:/)
|
|
317
|
+
error_message = error_lines.last.strip if error_lines.any?
|
|
318
|
+
error_found = true
|
|
319
|
+
break
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Check for SSH authentication messages
|
|
323
|
+
if !ssh_auth_detected && (log_content.include?('SSH') ||
|
|
324
|
+
log_content.include?('ssh') ||
|
|
325
|
+
log_content.include?('Authenticating') ||
|
|
326
|
+
log_content.include?('authentication') ||
|
|
327
|
+
log_content.include?('1Password') ||
|
|
328
|
+
@command.include?('kamal') ||
|
|
329
|
+
@command.include?('ssh'))
|
|
330
|
+
ssh_auth_detected = true
|
|
331
|
+
puts "SSH authentication detected, extending timeout..." if @verbose
|
|
332
|
+
# Extend deadline for SSH auth
|
|
333
|
+
deadline = Time.now + [timeout, 60].max
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Check if socket exists and server is responsive
|
|
339
|
+
if File.exist?(@socket_path)
|
|
340
|
+
begin
|
|
341
|
+
status = get_status
|
|
342
|
+
if status && status['success'] && status['running']
|
|
343
|
+
return true
|
|
344
|
+
end
|
|
345
|
+
rescue StandardError
|
|
346
|
+
# Socket exists but not ready yet, continue waiting
|
|
347
|
+
end
|
|
348
|
+
end
|
|
263
349
|
|
|
264
350
|
sleep 0.1
|
|
265
351
|
end
|
|
266
352
|
|
|
267
|
-
|
|
353
|
+
if error_found
|
|
354
|
+
raise "Server failed to start: #{error_message || 'Unknown error'}"
|
|
355
|
+
else
|
|
356
|
+
timeout_msg = "Server failed to start within #{timeout} seconds"
|
|
357
|
+
timeout_msg += " (SSH authentication may be required)" if ssh_auth_detected || @command.include?('ssh') || @command.include?('kamal')
|
|
358
|
+
raise timeout_msg
|
|
359
|
+
end
|
|
268
360
|
end
|
|
269
361
|
|
|
270
362
|
def send_request(request, timeout: 30)
|
data/lib/consolle/cli.rb
CHANGED
|
@@ -126,9 +126,13 @@ module Consolle
|
|
|
126
126
|
Custom console commands are supported for special environments:
|
|
127
127
|
cone start --command "kamal app exec -i 'bin/rails console'"
|
|
128
128
|
cone start --command "docker exec -it myapp bin/rails console"
|
|
129
|
+
|
|
130
|
+
For SSH-based commands that require authentication (e.g., 1Password SSH agent):
|
|
131
|
+
cone start --command "kamal console" --wait-timeout 60
|
|
129
132
|
LONGDESC
|
|
130
133
|
method_option :rails_env, type: :string, aliases: '-e', desc: 'Rails environment', default: 'development'
|
|
131
134
|
method_option :command, type: :string, aliases: '-c', desc: 'Custom console command', default: 'bin/rails console'
|
|
135
|
+
method_option :wait_timeout, type: :numeric, aliases: '-w', desc: 'Timeout for console startup (seconds)', default: 15
|
|
132
136
|
def start
|
|
133
137
|
ensure_rails_project!
|
|
134
138
|
ensure_project_directories
|
|
@@ -155,7 +159,7 @@ module Consolle
|
|
|
155
159
|
clear_session_info
|
|
156
160
|
end
|
|
157
161
|
|
|
158
|
-
adapter = create_rails_adapter(options[:rails_env], options[:target], options[:command])
|
|
162
|
+
adapter = create_rails_adapter(options[:rails_env], options[:target], options[:command], options[:wait_timeout])
|
|
159
163
|
|
|
160
164
|
puts 'Starting Rails console...'
|
|
161
165
|
|
|
@@ -553,9 +557,20 @@ module Consolle
|
|
|
553
557
|
puts result['result'] unless result['result'].nil?
|
|
554
558
|
puts "Execution time: #{result['execution_time']}s" if options[:verbose] && result['execution_time']
|
|
555
559
|
else
|
|
556
|
-
|
|
560
|
+
# Display error information
|
|
561
|
+
if result['error_code']
|
|
562
|
+
puts "Error: #{result['error_code']}"
|
|
563
|
+
else
|
|
564
|
+
puts "Error: #{result['error']}"
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Show error class in verbose mode
|
|
568
|
+
if options[:verbose] && result['error_class']
|
|
569
|
+
puts "Error Class: #{result['error_class']}"
|
|
570
|
+
end
|
|
571
|
+
|
|
557
572
|
puts result['message']
|
|
558
|
-
puts result['backtrace']&.join("\n") if options[:verbose]
|
|
573
|
+
puts result['backtrace']&.join("\n") if options[:verbose] && result['backtrace']
|
|
559
574
|
exit 1
|
|
560
575
|
end
|
|
561
576
|
end
|
|
@@ -633,7 +648,7 @@ module Consolle
|
|
|
633
648
|
File.join(Dir.pwd, 'tmp', 'cone', 'sessions.json')
|
|
634
649
|
end
|
|
635
650
|
|
|
636
|
-
def create_rails_adapter(rails_env = 'development', target = nil, command = nil)
|
|
651
|
+
def create_rails_adapter(rails_env = 'development', target = nil, command = nil, wait_timeout = nil)
|
|
637
652
|
target ||= options[:target]
|
|
638
653
|
|
|
639
654
|
Consolle::Adapters::RailsConsole.new(
|
|
@@ -643,7 +658,8 @@ module Consolle
|
|
|
643
658
|
rails_root: Dir.pwd,
|
|
644
659
|
rails_env: rails_env,
|
|
645
660
|
verbose: options[:verbose],
|
|
646
|
-
command: command
|
|
661
|
+
command: command,
|
|
662
|
+
wait_timeout: wait_timeout
|
|
647
663
|
)
|
|
648
664
|
end
|
|
649
665
|
|
|
@@ -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,7 @@ require 'pty'
|
|
|
4
4
|
require 'timeout'
|
|
5
5
|
require 'fcntl'
|
|
6
6
|
require 'logger'
|
|
7
|
+
require_relative '../errors'
|
|
7
8
|
|
|
8
9
|
# Ruby 3.4.0+ extracts base64 as a default gem
|
|
9
10
|
# Suppress warning by silencing verbose mode temporarily
|
|
@@ -26,11 +27,12 @@ module Consolle
|
|
|
26
27
|
PROMPT_PATTERN = /^[^\w]*(\u001E\u001F<CONSOLLE>\u001F\u001E|\w+[-_]?\w*\([^)]*\)>|irb\([^)]+\):\d+:?\d*[>*]|>>|>)\s*$/
|
|
27
28
|
CTRL_C = "\x03"
|
|
28
29
|
|
|
29
|
-
def initialize(rails_root:, rails_env: 'development', logger: nil, command: nil)
|
|
30
|
+
def initialize(rails_root:, rails_env: 'development', logger: nil, command: nil, wait_timeout: nil)
|
|
30
31
|
@rails_root = rails_root
|
|
31
32
|
@rails_env = rails_env
|
|
32
33
|
@command = command || 'bin/rails console'
|
|
33
34
|
@logger = logger || Logger.new(STDOUT)
|
|
35
|
+
@wait_timeout = wait_timeout || 15
|
|
34
36
|
@pid = nil
|
|
35
37
|
@reader = nil
|
|
36
38
|
@writer = nil
|
|
@@ -72,11 +74,31 @@ module Consolle
|
|
|
72
74
|
|
|
73
75
|
# Encode code using Base64 to handle special characters and remote consoles
|
|
74
76
|
# Ensure UTF-8 encoding to handle strings that may be tagged as ASCII-8BIT
|
|
75
|
-
utf8_code = code.encoding == Encoding::UTF_8
|
|
77
|
+
utf8_code = if code.encoding == Encoding::UTF_8
|
|
78
|
+
code
|
|
79
|
+
else
|
|
80
|
+
# Try to handle ASCII-8BIT strings that might contain UTF-8 data
|
|
81
|
+
temp = code.dup
|
|
82
|
+
if code.encoding == Encoding::ASCII_8BIT
|
|
83
|
+
# First try to interpret as UTF-8
|
|
84
|
+
temp.force_encoding('UTF-8')
|
|
85
|
+
if temp.valid_encoding?
|
|
86
|
+
temp
|
|
87
|
+
else
|
|
88
|
+
# If not valid UTF-8, try other common encodings
|
|
89
|
+
# or use replacement characters
|
|
90
|
+
code.encode('UTF-8', invalid: :replace, undef: :replace)
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
# For other encodings, convert to UTF-8
|
|
94
|
+
code.encode('UTF-8')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
76
97
|
encoded_code = Base64.strict_encode64(utf8_code)
|
|
77
98
|
|
|
78
|
-
# Use eval to execute the Base64-decoded code
|
|
79
|
-
|
|
99
|
+
# Use eval to execute the Base64-decoded code with exception handling
|
|
100
|
+
# Ensure decoded string is properly handled as UTF-8
|
|
101
|
+
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"
|
|
80
102
|
logger.debug '[ConsoleSupervisor] Sending eval command (Base64 encoded)'
|
|
81
103
|
@writer.puts eval_command
|
|
82
104
|
@writer.flush
|
|
@@ -93,11 +115,7 @@ module Consolle
|
|
|
93
115
|
@writer.flush
|
|
94
116
|
sleep 0.5
|
|
95
117
|
clear_buffer
|
|
96
|
-
return
|
|
97
|
-
success: false,
|
|
98
|
-
output: "Execution timed out after #{timeout} seconds",
|
|
99
|
-
execution_time: timeout
|
|
100
|
-
}
|
|
118
|
+
return build_timeout_response(timeout)
|
|
101
119
|
end
|
|
102
120
|
|
|
103
121
|
begin
|
|
@@ -122,25 +140,29 @@ module Consolle
|
|
|
122
140
|
# PTY can throw EIO when no data available
|
|
123
141
|
IO.select([@reader], nil, nil, 0.1)
|
|
124
142
|
rescue EOFError
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
output: 'Console terminated',
|
|
143
|
+
return build_error_response(
|
|
144
|
+
EOFError.new('Console terminated'),
|
|
128
145
|
execution_time: nil
|
|
129
|
-
|
|
146
|
+
)
|
|
130
147
|
end
|
|
131
148
|
end
|
|
132
149
|
|
|
133
150
|
# Parse and return result
|
|
134
|
-
|
|
151
|
+
parsed_result = parse_output(output, eval_command)
|
|
135
152
|
|
|
136
153
|
# Log for debugging object output issues
|
|
137
154
|
logger.debug "[ConsoleSupervisor] Raw output: #{output.inspect}"
|
|
138
|
-
logger.debug "[ConsoleSupervisor] Parsed result: #{
|
|
155
|
+
logger.debug "[ConsoleSupervisor] Parsed result: #{parsed_result.inspect}"
|
|
139
156
|
|
|
140
|
-
|
|
157
|
+
# Check if the output contains an error
|
|
158
|
+
if parsed_result.is_a?(Hash) && parsed_result[:error]
|
|
159
|
+
build_error_response(parsed_result[:exception], execution_time: nil)
|
|
160
|
+
else
|
|
161
|
+
{ success: true, output: parsed_result, execution_time: nil }
|
|
162
|
+
end
|
|
141
163
|
rescue StandardError => e
|
|
142
164
|
logger.error "[ConsoleSupervisor] Eval error: #{e.message}"
|
|
143
|
-
|
|
165
|
+
build_error_response(e, execution_time: nil)
|
|
144
166
|
end
|
|
145
167
|
end
|
|
146
168
|
end
|
|
@@ -184,6 +206,39 @@ module Consolle
|
|
|
184
206
|
|
|
185
207
|
private
|
|
186
208
|
|
|
209
|
+
def build_timeout_response(timeout_seconds)
|
|
210
|
+
error = Consolle::Errors::ExecutionTimeout.new(timeout_seconds)
|
|
211
|
+
{
|
|
212
|
+
success: false,
|
|
213
|
+
error_class: error.class.name,
|
|
214
|
+
error_code: Consolle::Errors::ErrorClassifier.to_code(error),
|
|
215
|
+
output: error.message,
|
|
216
|
+
execution_time: timeout_seconds
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def build_error_response(exception, execution_time: nil)
|
|
221
|
+
# Handle string messages that come from eval output parsing
|
|
222
|
+
if exception.is_a?(String)
|
|
223
|
+
error_code = Consolle::Errors::ErrorClassifier.classify_message(exception)
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
error_class: 'RuntimeError',
|
|
227
|
+
error_code: error_code,
|
|
228
|
+
output: exception,
|
|
229
|
+
execution_time: execution_time
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
{
|
|
234
|
+
success: false,
|
|
235
|
+
error_class: exception.class.name,
|
|
236
|
+
error_code: Consolle::Errors::ErrorClassifier.to_code(exception),
|
|
237
|
+
output: "#{exception.class}: #{exception.message}",
|
|
238
|
+
execution_time: execution_time
|
|
239
|
+
}
|
|
240
|
+
end
|
|
241
|
+
|
|
187
242
|
def spawn_console
|
|
188
243
|
env = {
|
|
189
244
|
'RAILS_ENV' => @rails_env,
|
|
@@ -226,7 +281,7 @@ module Consolle
|
|
|
226
281
|
trim_restart_history
|
|
227
282
|
|
|
228
283
|
# Wait for initial prompt
|
|
229
|
-
wait_for_prompt(timeout:
|
|
284
|
+
wait_for_prompt(timeout: @wait_timeout)
|
|
230
285
|
|
|
231
286
|
# Configure IRB settings for automation
|
|
232
287
|
configure_irb_for_automation
|
|
@@ -470,6 +525,7 @@ module Consolle
|
|
|
470
525
|
lines = clean.lines
|
|
471
526
|
result_lines = []
|
|
472
527
|
skip_echo = true
|
|
528
|
+
error_exception = nil
|
|
473
529
|
|
|
474
530
|
lines.each_with_index do |line, idx|
|
|
475
531
|
# Skip the eval command echo (both file-based and Base64)
|
|
@@ -489,12 +545,43 @@ module Consolle
|
|
|
489
545
|
next
|
|
490
546
|
end
|
|
491
547
|
|
|
548
|
+
# Check for error patterns
|
|
549
|
+
if !error_exception && line.match?(/^(.*Error|.*Exception):/)
|
|
550
|
+
error_match = line.match(/^((?:\w+::)*\w*(?:Error|Exception)):\s*(.*)/)
|
|
551
|
+
if error_match
|
|
552
|
+
error_class = error_match[1]
|
|
553
|
+
error_message = error_match[2]
|
|
554
|
+
|
|
555
|
+
# Try to create the actual exception object
|
|
556
|
+
begin
|
|
557
|
+
# Handle namespaced errors
|
|
558
|
+
if error_class.include?('::')
|
|
559
|
+
parts = error_class.split('::')
|
|
560
|
+
klass = Object
|
|
561
|
+
parts.each { |part| klass = klass.const_get(part) }
|
|
562
|
+
error_exception = klass.new(error_message)
|
|
563
|
+
else
|
|
564
|
+
# Try core Ruby error classes
|
|
565
|
+
error_exception = Object.const_get(error_class).new(error_message)
|
|
566
|
+
end
|
|
567
|
+
rescue NameError
|
|
568
|
+
# If we can't find the exact error class, use a generic RuntimeError
|
|
569
|
+
error_exception = RuntimeError.new("#{error_class}: #{error_message}")
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
492
574
|
# Collect all other lines (including return values and side effects)
|
|
493
575
|
result_lines << line
|
|
494
576
|
end
|
|
495
577
|
|
|
496
|
-
#
|
|
497
|
-
|
|
578
|
+
# If an error was detected, return it as a hash
|
|
579
|
+
if error_exception
|
|
580
|
+
{ error: true, exception: error_exception, output: result_lines.join.strip }
|
|
581
|
+
else
|
|
582
|
+
# Join all lines - this includes both side effects and return values
|
|
583
|
+
result_lines.join.strip
|
|
584
|
+
end
|
|
498
585
|
end
|
|
499
586
|
|
|
500
587
|
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,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'consolle/version'
|
|
4
|
+
require_relative 'consolle/errors'
|
|
4
5
|
require_relative 'consolle/cli'
|
|
5
6
|
|
|
6
7
|
# Server components
|
|
@@ -9,5 +10,4 @@ require_relative 'consolle/server/console_supervisor'
|
|
|
9
10
|
require_relative 'consolle/server/request_broker'
|
|
10
11
|
|
|
11
12
|
module Consolle
|
|
12
|
-
class Error < StandardError; end
|
|
13
13
|
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.2.
|
|
4
|
+
version: 0.2.9
|
|
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,7 @@ files:
|
|
|
79
79
|
- lib/consolle.rb
|
|
80
80
|
- lib/consolle/adapters/rails_console.rb
|
|
81
81
|
- lib/consolle/cli.rb
|
|
82
|
+
- lib/consolle/errors.rb
|
|
82
83
|
- lib/consolle/server/console_socket_server.rb
|
|
83
84
|
- lib/consolle/server/console_supervisor.rb
|
|
84
85
|
- lib/consolle/server/request_broker.rb
|