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.
@@ -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