consolle 0.2.6
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 +7 -0
- data/.github/workflows/test.yml +52 -0
- data/.gitignore +6 -0
- data/.mise.toml +9 -0
- data/.rspec +4 -0
- data/.version +1 -0
- data/CHANGELOG.md +29 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +39 -0
- data/LICENSE +9 -0
- data/README.md +190 -0
- data/bin/cone +6 -0
- data/bin/consolle +6 -0
- data/consolle.gemspec +40 -0
- data/lib/consolle/adapters/rails_console.rb +295 -0
- data/lib/consolle/cli.rb +718 -0
- data/lib/consolle/server/console_socket_server.rb +201 -0
- data/lib/consolle/server/console_supervisor.rb +556 -0
- data/lib/consolle/server/request_broker.rb +247 -0
- data/lib/consolle/version.rb +5 -0
- data/lib/consolle.rb +13 -0
- data/mise/release.sh +120 -0
- data/rule.ko.md +117 -0
- data/rule.md +117 -0
- metadata +115 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "json"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
|
|
9
|
+
module Consolle
|
|
10
|
+
module Adapters
|
|
11
|
+
class RailsConsole
|
|
12
|
+
attr_reader :socket_path, :process_pid, :pid_path, :log_path
|
|
13
|
+
|
|
14
|
+
def initialize(socket_path: nil, pid_path: nil, log_path: nil, rails_root: nil, rails_env: nil, verbose: false, command: nil)
|
|
15
|
+
@socket_path = socket_path || default_socket_path
|
|
16
|
+
@pid_path = pid_path || default_pid_path
|
|
17
|
+
@log_path = log_path || default_log_path
|
|
18
|
+
@rails_root = rails_root || Dir.pwd
|
|
19
|
+
@rails_env = rails_env || "development"
|
|
20
|
+
@verbose = verbose
|
|
21
|
+
@command = command || "bin/rails console"
|
|
22
|
+
@server_pid = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def start
|
|
26
|
+
return false if running?
|
|
27
|
+
|
|
28
|
+
# Start socket server daemon
|
|
29
|
+
start_server_daemon
|
|
30
|
+
|
|
31
|
+
# Wait for server to be ready
|
|
32
|
+
wait_for_server(timeout: 10)
|
|
33
|
+
|
|
34
|
+
# Get server status
|
|
35
|
+
status = get_status
|
|
36
|
+
@server_pid = status["pid"] if status && status["success"]
|
|
37
|
+
|
|
38
|
+
true
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
stop_server_daemon
|
|
41
|
+
raise e
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def stop
|
|
45
|
+
return false unless running?
|
|
46
|
+
|
|
47
|
+
stop_server_daemon
|
|
48
|
+
@server_pid = nil
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def restart
|
|
53
|
+
stop
|
|
54
|
+
sleep 1
|
|
55
|
+
start
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def running?
|
|
59
|
+
return false unless File.exist?(@socket_path)
|
|
60
|
+
|
|
61
|
+
# Check if socket is responsive
|
|
62
|
+
status = get_status
|
|
63
|
+
status && status["success"] && status["running"]
|
|
64
|
+
rescue StandardError
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def process_pid
|
|
69
|
+
# Return the server daemon PID from pid file
|
|
70
|
+
pid_file = @pid_path
|
|
71
|
+
return nil unless File.exist?(pid_file)
|
|
72
|
+
|
|
73
|
+
File.read(pid_file).strip.to_i
|
|
74
|
+
rescue StandardError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def send_code(code, timeout: 15)
|
|
79
|
+
unless running?
|
|
80
|
+
raise RuntimeError, "Console is not running"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
request = {
|
|
84
|
+
"action" => "eval",
|
|
85
|
+
"code" => code,
|
|
86
|
+
"timeout" => timeout,
|
|
87
|
+
"request_id" => SecureRandom.uuid
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
response = send_request(request, timeout: timeout + 5)
|
|
91
|
+
|
|
92
|
+
# Format response for compatibility
|
|
93
|
+
if response["success"]
|
|
94
|
+
{
|
|
95
|
+
"success" => true,
|
|
96
|
+
"result" => response["result"],
|
|
97
|
+
"execution_time" => response["execution_time"],
|
|
98
|
+
"request_id" => response["request_id"]
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
{
|
|
102
|
+
"success" => false,
|
|
103
|
+
"error" => response["error"] || "Unknown",
|
|
104
|
+
"message" => response["message"] || "Unknown error",
|
|
105
|
+
"request_id" => response["request_id"]
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def get_status
|
|
111
|
+
request = {
|
|
112
|
+
"action" => "status",
|
|
113
|
+
"request_id" => SecureRandom.uuid
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
send_request(request, timeout: 5)
|
|
117
|
+
rescue StandardError
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def default_socket_path
|
|
124
|
+
File.join(Dir.pwd, "tmp", "cone", "cone.socket")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def default_pid_path
|
|
128
|
+
File.join(Dir.pwd, "tmp", "cone", "cone.pid")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def default_log_path
|
|
132
|
+
File.join(Dir.pwd, "tmp", "cone", "cone.log")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def server_command
|
|
136
|
+
consolle_lib_path = File.expand_path("../..", __dir__)
|
|
137
|
+
|
|
138
|
+
# Build server command
|
|
139
|
+
[
|
|
140
|
+
"ruby",
|
|
141
|
+
"-I", consolle_lib_path,
|
|
142
|
+
"-e", server_script,
|
|
143
|
+
"--",
|
|
144
|
+
@socket_path,
|
|
145
|
+
@rails_root,
|
|
146
|
+
@rails_env,
|
|
147
|
+
@verbose ? "debug" : "info",
|
|
148
|
+
@pid_path,
|
|
149
|
+
@log_path,
|
|
150
|
+
@command
|
|
151
|
+
]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def server_script
|
|
155
|
+
<<~RUBY
|
|
156
|
+
begin
|
|
157
|
+
require 'consolle/server/console_socket_server'
|
|
158
|
+
require 'logger'
|
|
159
|
+
|
|
160
|
+
socket_path, rails_root, rails_env, log_level, pid_path, log_path, command = ARGV
|
|
161
|
+
|
|
162
|
+
# Write initial log
|
|
163
|
+
log_file = log_path || socket_path.sub(/\\.socket$/, '.log')
|
|
164
|
+
File.open(log_file, 'a') { |f| f.puts "[Server] Starting... PID: \#{Process.pid}" }
|
|
165
|
+
|
|
166
|
+
# Daemonize
|
|
167
|
+
Process.daemon(true, false)
|
|
168
|
+
|
|
169
|
+
# Redirect output
|
|
170
|
+
$stdout.reopen(log_file, 'a')
|
|
171
|
+
$stderr.reopen(log_file, 'a')
|
|
172
|
+
$stdout.sync = $stderr.sync = true
|
|
173
|
+
|
|
174
|
+
# Write PID file
|
|
175
|
+
pid_file = pid_path || socket_path.sub(/\\.socket$/, '.pid')
|
|
176
|
+
File.write(pid_file, Process.pid.to_s)
|
|
177
|
+
|
|
178
|
+
puts "[Server] Daemon started, PID: \#{Process.pid}"
|
|
179
|
+
|
|
180
|
+
# Create logger with appropriate level
|
|
181
|
+
logger = Logger.new(log_file)
|
|
182
|
+
logger.level = (log_level == 'debug') ? Logger::DEBUG : Logger::INFO
|
|
183
|
+
|
|
184
|
+
# Start server
|
|
185
|
+
server = Consolle::Server::ConsoleSocketServer.new(
|
|
186
|
+
socket_path: socket_path,
|
|
187
|
+
rails_root: rails_root,
|
|
188
|
+
rails_env: rails_env,
|
|
189
|
+
logger: logger,
|
|
190
|
+
command: command
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
puts "[Server] Starting server with log level: \#{log_level}..."
|
|
194
|
+
server.start
|
|
195
|
+
|
|
196
|
+
puts "[Server] Server started, entering sleep..."
|
|
197
|
+
|
|
198
|
+
# Keep running
|
|
199
|
+
sleep
|
|
200
|
+
rescue => e
|
|
201
|
+
puts "[Server] Error: \#{e.class}: \#{e.message}"
|
|
202
|
+
puts e.backtrace.join("\\n")
|
|
203
|
+
exit 1
|
|
204
|
+
end
|
|
205
|
+
RUBY
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def start_server_daemon
|
|
209
|
+
# Ensure directory exists
|
|
210
|
+
socket_dir = File.dirname(@socket_path)
|
|
211
|
+
FileUtils.mkdir_p(socket_dir) unless Dir.exist?(socket_dir)
|
|
212
|
+
|
|
213
|
+
# Start server process
|
|
214
|
+
log_file = @log_path
|
|
215
|
+
pid = Process.spawn(
|
|
216
|
+
*server_command,
|
|
217
|
+
out: log_file,
|
|
218
|
+
err: log_file
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
Process.detach(pid)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def stop_server_daemon
|
|
225
|
+
# Read PID file
|
|
226
|
+
pid_file = @pid_path
|
|
227
|
+
return unless File.exist?(pid_file)
|
|
228
|
+
|
|
229
|
+
pid = File.read(pid_file).to_i
|
|
230
|
+
|
|
231
|
+
# Kill process
|
|
232
|
+
begin
|
|
233
|
+
Process.kill("TERM", pid)
|
|
234
|
+
|
|
235
|
+
# Wait for socket to disappear
|
|
236
|
+
10.times do
|
|
237
|
+
break unless File.exist?(@socket_path)
|
|
238
|
+
sleep 0.1
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Force kill if needed
|
|
242
|
+
if File.exist?(@socket_path)
|
|
243
|
+
Process.kill("KILL", pid) rescue nil
|
|
244
|
+
end
|
|
245
|
+
rescue Errno::ESRCH
|
|
246
|
+
# Process already dead
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Clean up files
|
|
250
|
+
File.unlink(@socket_path) if File.exist?(@socket_path)
|
|
251
|
+
File.unlink(pid_file) if File.exist?(pid_file)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def wait_for_server(timeout: 10)
|
|
255
|
+
deadline = Time.now + timeout
|
|
256
|
+
|
|
257
|
+
while Time.now < deadline
|
|
258
|
+
return true if File.exist?(@socket_path) && get_status
|
|
259
|
+
sleep 0.1
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
raise "Server failed to start within #{timeout} seconds"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def send_request(request, timeout: 30)
|
|
266
|
+
Timeout.timeout(timeout) do
|
|
267
|
+
socket = UNIXSocket.new(@socket_path)
|
|
268
|
+
|
|
269
|
+
# Send request
|
|
270
|
+
socket.write(JSON.generate(request))
|
|
271
|
+
socket.write("\n")
|
|
272
|
+
socket.flush
|
|
273
|
+
|
|
274
|
+
# Read response
|
|
275
|
+
response_data = socket.gets
|
|
276
|
+
socket.close
|
|
277
|
+
|
|
278
|
+
JSON.parse(response_data)
|
|
279
|
+
end
|
|
280
|
+
rescue Timeout::Error
|
|
281
|
+
{
|
|
282
|
+
"success" => false,
|
|
283
|
+
"error" => "Timeout",
|
|
284
|
+
"message" => "Request timed out after #{timeout} seconds"
|
|
285
|
+
}
|
|
286
|
+
rescue StandardError => e
|
|
287
|
+
{
|
|
288
|
+
"success" => false,
|
|
289
|
+
"error" => e.class.name,
|
|
290
|
+
"message" => e.message
|
|
291
|
+
}
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|