consolle 0.3.1 → 0.3.3
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/cli.rb +31 -4
- data/lib/consolle/server/console_socket_server.rb +11 -3
- data/lib/consolle/server/console_supervisor.rb +91 -16
- data/lib/consolle/server/request_broker.rb +7 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b736640d04dff94d8b7999c5c1d0546e76476fa09261645041ce42315a4a81a1
|
|
4
|
+
data.tar.gz: 05e6672b68f0391004e292c9e73c61d6b0ce87e344ceed9318af4b65e54fd814
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '06863ea17a179c399c3e7b34187c636d73f6830da923c04516871102caa02df810799074b141b746f403b81aebcbe22a3ce3eb1e170592e43eeffb8a260a97ef'
|
|
7
|
+
data.tar.gz: ae90cbf05848abe0b29d4bd860287a18fe83b6907d404aba230505803a5d48d92cbc422d7c717bb3d46ee89777eebbcff3845f14877fd92b747f04b394474863
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.3
|
data/Gemfile.lock
CHANGED
data/lib/consolle/cli.rb
CHANGED
|
@@ -556,7 +556,8 @@ module Consolle
|
|
|
556
556
|
if result['success']
|
|
557
557
|
# Always print result, even if empty (multiline code often returns empty string)
|
|
558
558
|
puts result['result'] unless result['result'].nil?
|
|
559
|
-
|
|
559
|
+
# Always show execution time when available
|
|
560
|
+
puts "Execution time: #{result['execution_time'].round(3)}s" if result['execution_time']
|
|
560
561
|
else
|
|
561
562
|
# Display error information
|
|
562
563
|
if result['error_code']
|
|
@@ -572,6 +573,10 @@ module Consolle
|
|
|
572
573
|
|
|
573
574
|
puts result['message']
|
|
574
575
|
puts result['backtrace']&.join("\n") if options[:verbose] && result['backtrace']
|
|
576
|
+
|
|
577
|
+
# Show execution time for errors too
|
|
578
|
+
puts "Execution time: #{result['execution_time'].round(3)}s" if result['execution_time']
|
|
579
|
+
|
|
575
580
|
exit 1
|
|
576
581
|
end
|
|
577
582
|
end
|
|
@@ -618,6 +623,9 @@ module Consolle
|
|
|
618
623
|
|
|
619
624
|
def send_code_to_socket(socket_path, code, timeout: 15)
|
|
620
625
|
request_id = SecureRandom.uuid
|
|
626
|
+
# Ensure code is UTF-8 encoded
|
|
627
|
+
code = code.force_encoding('UTF-8') if code.respond_to?(:force_encoding)
|
|
628
|
+
|
|
621
629
|
request = {
|
|
622
630
|
'action' => 'eval',
|
|
623
631
|
'code' => code,
|
|
@@ -625,23 +633,42 @@ module Consolle
|
|
|
625
633
|
'request_id' => request_id
|
|
626
634
|
}
|
|
627
635
|
|
|
636
|
+
STDERR.puts "[DEBUG] Creating socket connection to: #{socket_path}" if ENV['DEBUG']
|
|
637
|
+
|
|
628
638
|
Timeout.timeout(timeout + 5) do
|
|
629
639
|
socket = UNIXSocket.new(socket_path)
|
|
640
|
+
STDERR.puts "[DEBUG] Socket connected" if ENV['DEBUG']
|
|
630
641
|
|
|
631
|
-
# Send request as single line JSON
|
|
632
|
-
|
|
642
|
+
# Send request as single line JSON with UTF-8 encoding
|
|
643
|
+
json_data = JSON.generate(request)
|
|
644
|
+
STDERR.puts "[DEBUG] JSON data size: #{json_data.bytesize} bytes" if ENV['DEBUG']
|
|
645
|
+
|
|
646
|
+
# Debug: Check for newlines in JSON
|
|
647
|
+
if ENV['DEBUG'] && json_data.include?("\n")
|
|
648
|
+
STDERR.puts "[DEBUG] WARNING: JSON contains literal newline!"
|
|
649
|
+
File.write("/tmp/cone_debug.json", json_data) if ENV['DEBUG_SAVE']
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
STDERR.puts "[DEBUG] Sending request..." if ENV['DEBUG']
|
|
653
|
+
|
|
654
|
+
socket.write(json_data)
|
|
633
655
|
socket.write("\n")
|
|
634
656
|
socket.flush
|
|
657
|
+
|
|
658
|
+
STDERR.puts "[DEBUG] Request sent, waiting for response..." if ENV['DEBUG']
|
|
635
659
|
|
|
636
660
|
# Read response
|
|
637
661
|
response_data = socket.gets
|
|
662
|
+
STDERR.puts "[DEBUG] Response received: #{response_data&.bytesize} bytes" if ENV['DEBUG']
|
|
638
663
|
socket.close
|
|
639
664
|
|
|
640
|
-
JSON.parse(response_data)
|
|
665
|
+
JSON.parse(response_data) if response_data
|
|
641
666
|
end
|
|
642
667
|
rescue Timeout::Error
|
|
668
|
+
STDERR.puts "[DEBUG] Timeout occurred after #{timeout} seconds" if ENV['DEBUG']
|
|
643
669
|
{ 'success' => false, 'error' => 'Timeout', 'message' => "Request timed out after #{timeout} seconds" }
|
|
644
670
|
rescue StandardError => e
|
|
671
|
+
STDERR.puts "[DEBUG] Error: #{e.class}: #{e.message}" if ENV['DEBUG']
|
|
645
672
|
{ 'success' => false, 'error' => e.class.name, 'message' => e.message }
|
|
646
673
|
end
|
|
647
674
|
|
|
@@ -146,18 +146,24 @@ module Consolle
|
|
|
146
146
|
def handle_client(client)
|
|
147
147
|
Thread.new do
|
|
148
148
|
# Read request
|
|
149
|
+
logger.debug "[ConsoleSocketServer] Waiting for request data..." if ENV['DEBUG']
|
|
149
150
|
request_data = client.gets
|
|
151
|
+
logger.debug "[ConsoleSocketServer] Received data: #{request_data&.bytesize} bytes" if ENV['DEBUG']
|
|
150
152
|
return unless request_data
|
|
151
153
|
|
|
152
154
|
request = JSON.parse(request_data)
|
|
153
155
|
logger.debug "[ConsoleSocketServer] Request: #{request.inspect}"
|
|
156
|
+
logger.debug "[ConsoleSocketServer] Code size: #{request['code']&.bytesize} bytes" if ENV['DEBUG'] && request['code']
|
|
154
157
|
|
|
155
158
|
# Process through broker
|
|
156
159
|
response = @broker.process_request(request)
|
|
157
160
|
|
|
158
161
|
# Send response
|
|
159
162
|
begin
|
|
160
|
-
|
|
163
|
+
# Ensure response is properly encoded as UTF-8
|
|
164
|
+
response_json = JSON.generate(response)
|
|
165
|
+
response_json = response_json.force_encoding('UTF-8')
|
|
166
|
+
client.write(response_json)
|
|
161
167
|
client.write("\n")
|
|
162
168
|
client.flush
|
|
163
169
|
rescue Errno::EPIPE
|
|
@@ -171,7 +177,8 @@ module Consolle
|
|
|
171
177
|
'error' => 'InvalidRequest',
|
|
172
178
|
'message' => "Invalid JSON: #{e.message}"
|
|
173
179
|
}
|
|
174
|
-
|
|
180
|
+
response_json = JSON.generate(error_response).force_encoding('UTF-8')
|
|
181
|
+
client.write(response_json)
|
|
175
182
|
client.write("\n")
|
|
176
183
|
rescue Errno::EPIPE
|
|
177
184
|
logger.debug '[ConsoleSocketServer] Client disconnected while sending JSON parse error'
|
|
@@ -187,7 +194,8 @@ module Consolle
|
|
|
187
194
|
'error' => e.class.name,
|
|
188
195
|
'message' => e.message
|
|
189
196
|
}
|
|
190
|
-
|
|
197
|
+
response_json = JSON.generate(error_response).force_encoding('UTF-8')
|
|
198
|
+
client.write(response_json)
|
|
191
199
|
client.write("\n")
|
|
192
200
|
rescue Errno::EPIPE
|
|
193
201
|
# Client disconnected while sending error response
|
|
@@ -51,6 +51,9 @@ module Consolle
|
|
|
51
51
|
@mutex.synchronize do
|
|
52
52
|
raise 'Console is not running' unless running?
|
|
53
53
|
|
|
54
|
+
# Record start time for execution measurement
|
|
55
|
+
start_time = Time.now
|
|
56
|
+
|
|
54
57
|
# Check if this is a remote console
|
|
55
58
|
is_remote = @command.include?('ssh') || @command.include?('kamal') || @command.include?('docker')
|
|
56
59
|
|
|
@@ -95,14 +98,59 @@ module Consolle
|
|
|
95
98
|
code.encode('UTF-8')
|
|
96
99
|
end
|
|
97
100
|
end
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
# For large code, use temporary file approach
|
|
102
|
+
if utf8_code.bytesize > 1000
|
|
103
|
+
logger.debug "[ConsoleSupervisor] Large code (#{utf8_code.bytesize} bytes), using temporary file approach"
|
|
104
|
+
|
|
105
|
+
# Create temp file with unique name
|
|
106
|
+
require 'tempfile'
|
|
107
|
+
require 'securerandom'
|
|
108
|
+
|
|
109
|
+
temp_filename = "consolle_temp_#{SecureRandom.hex(8)}.rb"
|
|
110
|
+
temp_path = if defined?(Rails) && Rails.root
|
|
111
|
+
Rails.root.join('tmp', temp_filename).to_s
|
|
112
|
+
else
|
|
113
|
+
File.join(Dir.tmpdir, temp_filename)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Write code to temp file
|
|
117
|
+
File.write(temp_path, utf8_code)
|
|
118
|
+
logger.debug "[ConsoleSupervisor] Wrote code to temp file: #{temp_path}"
|
|
119
|
+
|
|
120
|
+
# Load and execute the file with timeout
|
|
121
|
+
eval_command = <<~RUBY.strip
|
|
122
|
+
begin
|
|
123
|
+
require 'timeout'
|
|
124
|
+
_temp_file = '#{temp_path}'
|
|
125
|
+
Timeout.timeout(#{timeout - 1}) do
|
|
126
|
+
load _temp_file
|
|
127
|
+
end
|
|
128
|
+
rescue Timeout::Error => e
|
|
129
|
+
puts "Timeout::Error: Code execution timed out after #{timeout - 1} seconds"
|
|
130
|
+
nil
|
|
131
|
+
rescue Exception => e
|
|
132
|
+
puts "\#{e.class}: \#{e.message}"
|
|
133
|
+
puts e.backtrace.first(5).join("\\n") if e.backtrace
|
|
134
|
+
nil
|
|
135
|
+
ensure
|
|
136
|
+
File.unlink(_temp_file) if File.exist?(_temp_file)
|
|
137
|
+
end
|
|
138
|
+
RUBY
|
|
139
|
+
|
|
140
|
+
@writer.puts eval_command
|
|
141
|
+
@writer.flush
|
|
142
|
+
else
|
|
143
|
+
# For smaller code, use Base64 encoding to avoid escaping issues
|
|
144
|
+
encoded_code = Base64.strict_encode64(utf8_code)
|
|
145
|
+
eval_command = "begin; require 'timeout'; Timeout.timeout(#{timeout - 1}) { eval(Base64.decode64('#{encoded_code}').force_encoding('UTF-8'), IRB.CurrentContext.workspace.binding) }; rescue Timeout::Error => e; puts \"Timeout::Error: Code execution timed out after #{timeout - 1} seconds\"; nil; rescue Exception => e; puts \"\#{e.class}: \#{e.message}\"; nil; end"
|
|
146
|
+
logger.debug "[ConsoleSupervisor] Small code (#{encoded_code.bytesize} bytes), using direct Base64 approach"
|
|
147
|
+
@writer.puts eval_command
|
|
148
|
+
@writer.flush
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
logger.debug "[ConsoleSupervisor] Code preview (first 100 chars): #{utf8_code[0..100].inspect}" if ENV['DEBUG']
|
|
152
|
+
|
|
153
|
+
logger.debug "[ConsoleSupervisor] Command sent at #{Time.now}, waiting for response..."
|
|
106
154
|
|
|
107
155
|
# Collect output
|
|
108
156
|
output = +''
|
|
@@ -111,17 +159,28 @@ module Consolle
|
|
|
111
159
|
begin
|
|
112
160
|
loop do
|
|
113
161
|
if Time.now > deadline
|
|
162
|
+
logger.debug "[ConsoleSupervisor] Timeout reached after #{Time.now - start_time}s, output so far: #{output.bytesize} bytes"
|
|
163
|
+
logger.debug "[ConsoleSupervisor] Output content: #{output.inspect}" if ENV['DEBUG']
|
|
114
164
|
# Timeout - send Ctrl-C
|
|
115
165
|
@writer.write(CTRL_C)
|
|
116
166
|
@writer.flush
|
|
117
167
|
sleep 0.5
|
|
118
168
|
clear_buffer
|
|
169
|
+
execution_time = Time.now - start_time
|
|
119
170
|
return build_timeout_response(timeout)
|
|
120
171
|
end
|
|
121
172
|
|
|
122
173
|
begin
|
|
123
174
|
chunk = @reader.read_nonblock(4096)
|
|
124
175
|
output << chunk
|
|
176
|
+
logger.debug "[ConsoleSupervisor] Got #{chunk.bytesize} bytes, total output: #{output.bytesize} bytes" if ENV['DEBUG']
|
|
177
|
+
|
|
178
|
+
# Respond to cursor position request during command execution
|
|
179
|
+
if chunk.include?("\e[6n")
|
|
180
|
+
logger.debug "[ConsoleSupervisor] Detected cursor position request during eval, sending response"
|
|
181
|
+
@writer.write("\e[1;1R")
|
|
182
|
+
@writer.flush
|
|
183
|
+
end
|
|
125
184
|
|
|
126
185
|
# Check if we got prompt back
|
|
127
186
|
clean = strip_ansi(output)
|
|
@@ -136,14 +195,16 @@ module Consolle
|
|
|
136
195
|
break
|
|
137
196
|
end
|
|
138
197
|
rescue IO::WaitReadable
|
|
198
|
+
logger.debug "[ConsoleSupervisor] Waiting for data... (#{Time.now - start_time}s elapsed, output size: #{output.bytesize})" if ENV['DEBUG']
|
|
139
199
|
IO.select([@reader], nil, nil, 0.1)
|
|
140
200
|
rescue Errno::EIO
|
|
141
201
|
# PTY can throw EIO when no data available
|
|
142
202
|
IO.select([@reader], nil, nil, 0.1)
|
|
143
203
|
rescue EOFError
|
|
204
|
+
execution_time = Time.now - start_time
|
|
144
205
|
return build_error_response(
|
|
145
206
|
EOFError.new('Console terminated'),
|
|
146
|
-
execution_time:
|
|
207
|
+
execution_time: execution_time
|
|
147
208
|
)
|
|
148
209
|
end
|
|
149
210
|
end
|
|
@@ -155,15 +216,19 @@ module Consolle
|
|
|
155
216
|
logger.debug "[ConsoleSupervisor] Raw output: #{output.inspect}"
|
|
156
217
|
logger.debug "[ConsoleSupervisor] Parsed result: #{parsed_result.inspect}"
|
|
157
218
|
|
|
219
|
+
# Calculate execution time
|
|
220
|
+
execution_time = Time.now - start_time
|
|
221
|
+
|
|
158
222
|
# Check if the output contains an error
|
|
159
223
|
if parsed_result.is_a?(Hash) && parsed_result[:error]
|
|
160
|
-
build_error_response(parsed_result[:exception], execution_time:
|
|
224
|
+
build_error_response(parsed_result[:exception], execution_time: execution_time)
|
|
161
225
|
else
|
|
162
|
-
{ success: true, output: parsed_result, execution_time:
|
|
226
|
+
{ success: true, output: parsed_result, execution_time: execution_time }
|
|
163
227
|
end
|
|
164
228
|
rescue StandardError => e
|
|
165
229
|
logger.error "[ConsoleSupervisor] Eval error: #{e.message}"
|
|
166
|
-
|
|
230
|
+
execution_time = Time.now - start_time
|
|
231
|
+
build_error_response(e, execution_time: execution_time)
|
|
167
232
|
end
|
|
168
233
|
end
|
|
169
234
|
end
|
|
@@ -250,8 +315,10 @@ module Consolle
|
|
|
250
315
|
# Disable pry-rails (force IRB instead of Pry)
|
|
251
316
|
'DISABLE_PRY_RAILS' => '1',
|
|
252
317
|
|
|
253
|
-
# Completely disable pager
|
|
318
|
+
# Completely disable pager (critical for automation)
|
|
254
319
|
'PAGER' => 'cat', # Set pager to cat (immediate output)
|
|
320
|
+
'GEM_PAGER' => 'cat', # Disable gem pager
|
|
321
|
+
'IRB_PAGER' => 'cat', # Ruby 3.3+ specific pager setting
|
|
255
322
|
'NO_PAGER' => '1', # Pager disable flag
|
|
256
323
|
'LESS' => '', # Clear less pager options
|
|
257
324
|
|
|
@@ -456,7 +523,13 @@ module Consolle
|
|
|
456
523
|
3.times do |i|
|
|
457
524
|
begin
|
|
458
525
|
loop do
|
|
459
|
-
@reader.read_nonblock(4096)
|
|
526
|
+
chunk = @reader.read_nonblock(4096)
|
|
527
|
+
# Respond to cursor position request even while clearing buffer
|
|
528
|
+
if chunk.include?("\e[6n")
|
|
529
|
+
logger.debug "[ConsoleSupervisor] Detected cursor position request during clear_buffer, sending response"
|
|
530
|
+
@writer.write("\e[1;1R")
|
|
531
|
+
@writer.flush
|
|
532
|
+
end
|
|
460
533
|
end
|
|
461
534
|
rescue IO::WaitReadable, Errno::EIO
|
|
462
535
|
# Buffer cleared for this iteration
|
|
@@ -471,6 +544,9 @@ module Consolle
|
|
|
471
544
|
|
|
472
545
|
# Send IRB configuration commands to disable interactive features
|
|
473
546
|
irb_commands = [
|
|
547
|
+
# CRITICAL: Disable pager first to prevent hanging on large outputs
|
|
548
|
+
'IRB.conf[:USE_PAGER] = false', # Disable pager completely
|
|
549
|
+
|
|
474
550
|
# Configure custom prompt mode to eliminate continuation prompts
|
|
475
551
|
'IRB.conf[:PROMPT][:CONSOLLE] = { ' \
|
|
476
552
|
'AUTO_INDENT: false, ' \
|
|
@@ -481,8 +557,7 @@ module Consolle
|
|
|
481
557
|
'RETURN: "=> %s\\n" }',
|
|
482
558
|
'IRB.conf[:PROMPT_MODE] = :CONSOLLE',
|
|
483
559
|
|
|
484
|
-
# Disable interactive features
|
|
485
|
-
'IRB.conf[:USE_PAGER] = false', # Disable pager
|
|
560
|
+
# Disable other interactive features
|
|
486
561
|
'IRB.conf[:USE_COLORIZE] = false', # Disable color output
|
|
487
562
|
'IRB.conf[:USE_AUTOCOMPLETE] = false', # Disable autocompletion
|
|
488
563
|
'IRB.conf[:USE_MULTILINE] = false', # Disable multiline editor to process code at once
|
|
@@ -42,6 +42,7 @@ module Consolle
|
|
|
42
42
|
|
|
43
43
|
def process_request(request)
|
|
44
44
|
request_id = request['request_id'] || SecureRandom.uuid
|
|
45
|
+
logger.debug "[RequestBroker] Received request: #{request_id}, action: #{request['action']}" if ENV['DEBUG']
|
|
45
46
|
|
|
46
47
|
# Create future for response
|
|
47
48
|
future = RequestFuture.new
|
|
@@ -57,11 +58,16 @@ module Consolle
|
|
|
57
58
|
request: request,
|
|
58
59
|
timestamp: Time.now
|
|
59
60
|
})
|
|
61
|
+
logger.debug "[RequestBroker] Queued request: #{request_id}, queue size: #{@queue.size}" if ENV['DEBUG']
|
|
60
62
|
|
|
61
63
|
# Wait for response (with timeout)
|
|
62
64
|
begin
|
|
63
|
-
|
|
65
|
+
logger.debug "[RequestBroker] Waiting for response: #{request_id}, timeout: #{request['timeout'] || 30}" if ENV['DEBUG']
|
|
66
|
+
response = future.get(timeout: request['timeout'] || 30)
|
|
67
|
+
logger.debug "[RequestBroker] Got response: #{request_id}" if ENV['DEBUG']
|
|
68
|
+
response
|
|
64
69
|
rescue Timeout::Error
|
|
70
|
+
logger.debug "[RequestBroker] Request timed out: #{request_id}" if ENV['DEBUG']
|
|
65
71
|
{
|
|
66
72
|
'success' => false,
|
|
67
73
|
'error' => 'RequestTimeout',
|
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.3.
|
|
4
|
+
version: 0.3.3
|
|
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-08-07 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: logger
|