consolle 0.3.2 → 0.3.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cbcbc211408bd181f9bb30dc1e20ad0c4798fd3501b4b5b5e4e157c1ed63850
4
- data.tar.gz: 71770bb16b893aa72b3c72edd8fff3f0f21c56596e7c1eac914b3f1981e1d8dd
3
+ metadata.gz: 4cf7b0621257f76801c7a5f4f064c511be503aadc56abf6a396d0fac9017506a
4
+ data.tar.gz: 3b393a2a0e389ebf93172ed7f1d38edb77fd1daa9eaf56f759200919683c1a04
5
5
  SHA512:
6
- metadata.gz: 37f73c3bccdb446557479ea5bbccd40bf70d9b95d306d49fd6f997911183b61cffb1042625953991b52fc99a383857090dc2170d24b13720adbb112b284b3886
7
- data.tar.gz: e8d66da8e20ee744762ae50bc9f7b43d4a56be28bef75ace9a7e8737cd6e3491420a615120b798d118fbe97a1fb30e0918353c26053a036fe8186770bb5b7bf3
6
+ metadata.gz: 992c65047ff3db88c005465133723c75d20841e13519c39e6cbae63f63abb2f61da8f11ea624763fa9d0bd220d27cd58ba8420769634a339e02a860706966bf5
7
+ data.tar.gz: 5459c740c89fcf7df8250348cea205cb4d8d6db52d6b50e2e3b695efa8998fa421936e363c2fc3559e6f666c4cd0f4b403dcab023c162984194cb8e31eddef6a
data/.version CHANGED
@@ -1 +1 @@
1
- 0.3.2
1
+ 0.3.4
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- consolle (0.3.2)
4
+ consolle (0.3.4)
5
5
  logger (~> 1.0)
6
6
  thor (~> 1.0)
7
7
 
@@ -78,7 +78,7 @@ module Consolle
78
78
  nil
79
79
  end
80
80
 
81
- def send_code(code, timeout: 15)
81
+ def send_code(code, timeout: 30)
82
82
  raise 'Console is not running' unless running?
83
83
 
84
84
  request = {
data/lib/consolle/cli.rb CHANGED
@@ -484,7 +484,7 @@ module Consolle
484
484
  #{' '}
485
485
  The console must be started first with 'cone start'.
486
486
  LONGDESC
487
- method_option :timeout, type: :numeric, desc: 'Timeout in seconds', default: 15
487
+ method_option :timeout, type: :numeric, desc: 'Timeout in seconds', default: 30
488
488
  method_option :file, type: :string, aliases: '-f', desc: 'Read Ruby code from FILE'
489
489
  method_option :raw, type: :boolean, desc: 'Do not apply escape fixes for Claude Code (keep \\! as is)'
490
490
  def exec(*code_parts)
@@ -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
- puts "Execution time: #{result['execution_time']}s" if options[:verbose] && result['execution_time']
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
@@ -616,8 +621,11 @@ module Consolle
616
621
  File.join(Dir.pwd, 'tmp', 'cone', "#{target}.log")
617
622
  end
618
623
 
619
- def send_code_to_socket(socket_path, code, timeout: 15)
624
+ def send_code_to_socket(socket_path, code, timeout: 30)
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,57 @@ 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
- socket.write(JSON.generate(request))
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
- # Read response
637
- response_data = socket.gets
660
+ # Read response - handle large responses by reading all available data
661
+ response_data = ''
662
+ begin
663
+ # Read until we get a newline (end of JSON response)
664
+ while (chunk = socket.read_nonblock(65536)) # Read in 64KB chunks
665
+ response_data << chunk
666
+ break if response_data.include?("\n")
667
+ end
668
+ rescue IO::WaitReadable
669
+ IO.select([socket], nil, nil, 1)
670
+ retry if response_data.empty? || !response_data.include?("\n")
671
+ rescue EOFError
672
+ # Server closed connection
673
+ end
674
+
675
+ STDERR.puts "[DEBUG] Response received: #{response_data&.bytesize} bytes" if ENV['DEBUG']
638
676
  socket.close
639
677
 
640
- JSON.parse(response_data)
678
+ # Extract just the first line (the JSON response)
679
+ json_line = response_data.split("\n").first
680
+ JSON.parse(json_line) if json_line
641
681
  end
642
682
  rescue Timeout::Error
683
+ STDERR.puts "[DEBUG] Timeout occurred after #{timeout} seconds" if ENV['DEBUG']
643
684
  { 'success' => false, 'error' => 'Timeout', 'message' => "Request timed out after #{timeout} seconds" }
644
685
  rescue StandardError => e
686
+ STDERR.puts "[DEBUG] Error: #{e.class}: #{e.message}" if ENV['DEBUG']
645
687
  { 'success' => false, 'error' => e.class.name, 'message' => e.message }
646
688
  end
647
689
 
@@ -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
- client.write(JSON.generate(response))
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
- client.write(JSON.generate(error_response))
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
- client.write(JSON.generate(error_response))
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
@@ -47,10 +47,16 @@ module Consolle
47
47
  start_watchdog
48
48
  end
49
49
 
50
- def eval(code, timeout: 30)
50
+ def eval(code, timeout: nil)
51
+ # Allow timeout to be configured via environment variable
52
+ default_timeout = ENV['CONSOLLE_TIMEOUT'] ? ENV['CONSOLLE_TIMEOUT'].to_i : 30
53
+ timeout ||= default_timeout
51
54
  @mutex.synchronize do
52
55
  raise 'Console is not running' unless running?
53
56
 
57
+ # Record start time for execution measurement
58
+ start_time = Time.now
59
+
54
60
  # Check if this is a remote console
55
61
  is_remote = @command.include?('ssh') || @command.include?('kamal') || @command.include?('docker')
56
62
 
@@ -95,14 +101,59 @@ module Consolle
95
101
  code.encode('UTF-8')
96
102
  end
97
103
  end
98
- encoded_code = Base64.strict_encode64(utf8_code)
99
-
100
- # Use eval to execute the Base64-decoded code with exception handling
101
- # Ensure decoded string is properly handled as UTF-8
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"
103
- logger.debug '[ConsoleSupervisor] Sending eval command (Base64 encoded)'
104
- @writer.puts eval_command
105
- @writer.flush
104
+ # For large code, use temporary file approach
105
+ if utf8_code.bytesize > 1000
106
+ logger.debug "[ConsoleSupervisor] Large code (#{utf8_code.bytesize} bytes), using temporary file approach"
107
+
108
+ # Create temp file with unique name
109
+ require 'tempfile'
110
+ require 'securerandom'
111
+
112
+ temp_filename = "consolle_temp_#{SecureRandom.hex(8)}.rb"
113
+ temp_path = if defined?(Rails) && Rails.root
114
+ Rails.root.join('tmp', temp_filename).to_s
115
+ else
116
+ File.join(Dir.tmpdir, temp_filename)
117
+ end
118
+
119
+ # Write code to temp file
120
+ File.write(temp_path, utf8_code)
121
+ logger.debug "[ConsoleSupervisor] Wrote code to temp file: #{temp_path}"
122
+
123
+ # Load and execute the file with timeout
124
+ eval_command = <<~RUBY.strip
125
+ begin
126
+ require 'timeout'
127
+ _temp_file = '#{temp_path}'
128
+ Timeout.timeout(#{timeout - 1}) do
129
+ load _temp_file
130
+ end
131
+ rescue Timeout::Error => e
132
+ puts "Timeout::Error: Code execution timed out after #{timeout - 1} seconds"
133
+ nil
134
+ rescue Exception => e
135
+ puts "\#{e.class}: \#{e.message}"
136
+ puts e.backtrace.first(5).join("\\n") if e.backtrace
137
+ nil
138
+ ensure
139
+ File.unlink(_temp_file) if File.exist?(_temp_file)
140
+ end
141
+ RUBY
142
+
143
+ @writer.puts eval_command
144
+ @writer.flush
145
+ else
146
+ # For smaller code, use Base64 encoding to avoid escaping issues
147
+ encoded_code = Base64.strict_encode64(utf8_code)
148
+ 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"
149
+ logger.debug "[ConsoleSupervisor] Small code (#{encoded_code.bytesize} bytes), using direct Base64 approach"
150
+ @writer.puts eval_command
151
+ @writer.flush
152
+ end
153
+
154
+ logger.debug "[ConsoleSupervisor] Code preview (first 100 chars): #{utf8_code[0..100].inspect}" if ENV['DEBUG']
155
+
156
+ logger.debug "[ConsoleSupervisor] Command sent at #{Time.now}, waiting for response..."
106
157
 
107
158
  # Collect output
108
159
  output = +''
@@ -111,17 +162,21 @@ module Consolle
111
162
  begin
112
163
  loop do
113
164
  if Time.now > deadline
165
+ logger.debug "[ConsoleSupervisor] Timeout reached after #{Time.now - start_time}s, output so far: #{output.bytesize} bytes"
166
+ logger.debug "[ConsoleSupervisor] Output content: #{output.inspect}" if ENV['DEBUG']
114
167
  # Timeout - send Ctrl-C
115
168
  @writer.write(CTRL_C)
116
169
  @writer.flush
117
170
  sleep 0.5
118
171
  clear_buffer
172
+ execution_time = Time.now - start_time
119
173
  return build_timeout_response(timeout)
120
174
  end
121
175
 
122
176
  begin
123
177
  chunk = @reader.read_nonblock(4096)
124
178
  output << chunk
179
+ logger.debug "[ConsoleSupervisor] Got #{chunk.bytesize} bytes, total output: #{output.bytesize} bytes" if ENV['DEBUG']
125
180
 
126
181
  # Respond to cursor position request during command execution
127
182
  if chunk.include?("\e[6n")
@@ -143,18 +198,30 @@ module Consolle
143
198
  break
144
199
  end
145
200
  rescue IO::WaitReadable
201
+ logger.debug "[ConsoleSupervisor] Waiting for data... (#{Time.now - start_time}s elapsed, output size: #{output.bytesize})" if ENV['DEBUG']
146
202
  IO.select([@reader], nil, nil, 0.1)
147
203
  rescue Errno::EIO
148
204
  # PTY can throw EIO when no data available
149
205
  IO.select([@reader], nil, nil, 0.1)
150
206
  rescue EOFError
207
+ execution_time = Time.now - start_time
151
208
  return build_error_response(
152
209
  EOFError.new('Console terminated'),
153
- execution_time: nil
210
+ execution_time: execution_time
154
211
  )
155
212
  end
156
213
  end
157
214
 
215
+ # Check if output is too large and truncate if necessary
216
+ max_output_size = 100_000 # 100KB limit for output
217
+ truncated = false
218
+
219
+ if output.bytesize > max_output_size
220
+ logger.warn "[ConsoleSupervisor] Output too large (#{output.bytesize} bytes), truncating to #{max_output_size} bytes"
221
+ output = output[0...max_output_size]
222
+ truncated = true
223
+ end
224
+
158
225
  # Parse and return result
159
226
  parsed_result = parse_output(output, eval_command)
160
227
 
@@ -162,15 +229,22 @@ module Consolle
162
229
  logger.debug "[ConsoleSupervisor] Raw output: #{output.inspect}"
163
230
  logger.debug "[ConsoleSupervisor] Parsed result: #{parsed_result.inspect}"
164
231
 
232
+ # Calculate execution time
233
+ execution_time = Time.now - start_time
234
+
165
235
  # Check if the output contains an error
166
236
  if parsed_result.is_a?(Hash) && parsed_result[:error]
167
- build_error_response(parsed_result[:exception], execution_time: nil)
237
+ build_error_response(parsed_result[:exception], execution_time: execution_time)
168
238
  else
169
- { success: true, output: parsed_result, execution_time: nil }
239
+ result = { success: true, output: parsed_result, execution_time: execution_time }
240
+ result[:truncated] = true if truncated
241
+ result[:truncated_at] = max_output_size if truncated
242
+ result
170
243
  end
171
244
  rescue StandardError => e
172
245
  logger.error "[ConsoleSupervisor] Eval error: #{e.message}"
173
- build_error_response(e, execution_time: nil)
246
+ execution_time = Time.now - start_time
247
+ build_error_response(e, execution_time: execution_time)
174
248
  end
175
249
  end
176
250
  end
@@ -257,8 +331,10 @@ module Consolle
257
331
  # Disable pry-rails (force IRB instead of Pry)
258
332
  'DISABLE_PRY_RAILS' => '1',
259
333
 
260
- # Completely disable pager
334
+ # Completely disable pager (critical for automation)
261
335
  'PAGER' => 'cat', # Set pager to cat (immediate output)
336
+ 'GEM_PAGER' => 'cat', # Disable gem pager
337
+ 'IRB_PAGER' => 'cat', # Ruby 3.3+ specific pager setting
262
338
  'NO_PAGER' => '1', # Pager disable flag
263
339
  'LESS' => '', # Clear less pager options
264
340
 
@@ -484,6 +560,9 @@ module Consolle
484
560
 
485
561
  # Send IRB configuration commands to disable interactive features
486
562
  irb_commands = [
563
+ # CRITICAL: Disable pager first to prevent hanging on large outputs
564
+ 'IRB.conf[:USE_PAGER] = false', # Disable pager completely
565
+
487
566
  # Configure custom prompt mode to eliminate continuation prompts
488
567
  'IRB.conf[:PROMPT][:CONSOLLE] = { ' \
489
568
  'AUTO_INDENT: false, ' \
@@ -494,8 +573,7 @@ module Consolle
494
573
  'RETURN: "=> %s\\n" }',
495
574
  'IRB.conf[:PROMPT_MODE] = :CONSOLLE',
496
575
 
497
- # Disable interactive features
498
- 'IRB.conf[:USE_PAGER] = false', # Disable pager
576
+ # Disable other interactive features
499
577
  'IRB.conf[:USE_COLORIZE] = false', # Disable color output
500
578
  'IRB.conf[:USE_AUTOCOMPLETE] = false', # Disable autocompletion
501
579
  '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
- future.get(timeout: request['timeout'] || 30)
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.2
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - nacyot
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-07-26 00:00:00.000000000 Z
10
+ date: 2025-08-07 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: logger