consolle 0.3.2 → 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 +77 -15
- 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,21 @@ 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']
|
|
125
177
|
|
|
126
178
|
# Respond to cursor position request during command execution
|
|
127
179
|
if chunk.include?("\e[6n")
|
|
@@ -143,14 +195,16 @@ module Consolle
|
|
|
143
195
|
break
|
|
144
196
|
end
|
|
145
197
|
rescue IO::WaitReadable
|
|
198
|
+
logger.debug "[ConsoleSupervisor] Waiting for data... (#{Time.now - start_time}s elapsed, output size: #{output.bytesize})" if ENV['DEBUG']
|
|
146
199
|
IO.select([@reader], nil, nil, 0.1)
|
|
147
200
|
rescue Errno::EIO
|
|
148
201
|
# PTY can throw EIO when no data available
|
|
149
202
|
IO.select([@reader], nil, nil, 0.1)
|
|
150
203
|
rescue EOFError
|
|
204
|
+
execution_time = Time.now - start_time
|
|
151
205
|
return build_error_response(
|
|
152
206
|
EOFError.new('Console terminated'),
|
|
153
|
-
execution_time:
|
|
207
|
+
execution_time: execution_time
|
|
154
208
|
)
|
|
155
209
|
end
|
|
156
210
|
end
|
|
@@ -162,15 +216,19 @@ module Consolle
|
|
|
162
216
|
logger.debug "[ConsoleSupervisor] Raw output: #{output.inspect}"
|
|
163
217
|
logger.debug "[ConsoleSupervisor] Parsed result: #{parsed_result.inspect}"
|
|
164
218
|
|
|
219
|
+
# Calculate execution time
|
|
220
|
+
execution_time = Time.now - start_time
|
|
221
|
+
|
|
165
222
|
# Check if the output contains an error
|
|
166
223
|
if parsed_result.is_a?(Hash) && parsed_result[:error]
|
|
167
|
-
build_error_response(parsed_result[:exception], execution_time:
|
|
224
|
+
build_error_response(parsed_result[:exception], execution_time: execution_time)
|
|
168
225
|
else
|
|
169
|
-
{ success: true, output: parsed_result, execution_time:
|
|
226
|
+
{ success: true, output: parsed_result, execution_time: execution_time }
|
|
170
227
|
end
|
|
171
228
|
rescue StandardError => e
|
|
172
229
|
logger.error "[ConsoleSupervisor] Eval error: #{e.message}"
|
|
173
|
-
|
|
230
|
+
execution_time = Time.now - start_time
|
|
231
|
+
build_error_response(e, execution_time: execution_time)
|
|
174
232
|
end
|
|
175
233
|
end
|
|
176
234
|
end
|
|
@@ -257,8 +315,10 @@ module Consolle
|
|
|
257
315
|
# Disable pry-rails (force IRB instead of Pry)
|
|
258
316
|
'DISABLE_PRY_RAILS' => '1',
|
|
259
317
|
|
|
260
|
-
# Completely disable pager
|
|
318
|
+
# Completely disable pager (critical for automation)
|
|
261
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
|
|
262
322
|
'NO_PAGER' => '1', # Pager disable flag
|
|
263
323
|
'LESS' => '', # Clear less pager options
|
|
264
324
|
|
|
@@ -484,6 +544,9 @@ module Consolle
|
|
|
484
544
|
|
|
485
545
|
# Send IRB configuration commands to disable interactive features
|
|
486
546
|
irb_commands = [
|
|
547
|
+
# CRITICAL: Disable pager first to prevent hanging on large outputs
|
|
548
|
+
'IRB.conf[:USE_PAGER] = false', # Disable pager completely
|
|
549
|
+
|
|
487
550
|
# Configure custom prompt mode to eliminate continuation prompts
|
|
488
551
|
'IRB.conf[:PROMPT][:CONSOLLE] = { ' \
|
|
489
552
|
'AUTO_INDENT: false, ' \
|
|
@@ -494,8 +557,7 @@ module Consolle
|
|
|
494
557
|
'RETURN: "=> %s\\n" }',
|
|
495
558
|
'IRB.conf[:PROMPT_MODE] = :CONSOLLE',
|
|
496
559
|
|
|
497
|
-
# Disable interactive features
|
|
498
|
-
'IRB.conf[:USE_PAGER] = false', # Disable pager
|
|
560
|
+
# Disable other interactive features
|
|
499
561
|
'IRB.conf[:USE_COLORIZE] = false', # Disable color output
|
|
500
562
|
'IRB.conf[:USE_AUTOCOMPLETE] = false', # Disable autocompletion
|
|
501
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
|