Skip to content

Commit 8830443

Browse files
committed
Automatically name forked processes based on callers
1 parent 6473287 commit 8830443

File tree

2 files changed

+108
-2
lines changed

2 files changed

+108
-2
lines changed

lib/ruby_lsp/ruby_lsp_rails/server.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,25 @@ def database_supports_indexing?(model)
529529
end
530530
end
531531

532+
# Patch fork to name processes based on the caller's file path. This is useful for figuring out what is creating more
533+
# child processes from the runtime server, so that we can optimize and more easily debug orphaned processes
534+
# @requires_ancestor: Kernel
535+
module ForkHook
536+
#: (*untyped) -> Integer?
537+
def _fork(*args)
538+
pid = super
539+
540+
if pid == 0
541+
fork_caller = caller_locations(1, 1)&.first
542+
Process.setproctitle("ruby-lsp-rails: #{fork_caller.path}") if fork_caller
543+
end
544+
545+
pid
546+
end
547+
548+
Process.singleton_class.prepend(self)
549+
end
550+
532551
if ARGV.first == "start"
533552
RubyLsp::Rails::Server.new(capabilities: JSON.parse(ARGV[1], symbolize_names: true)).start
534553
end

test/ruby_lsp_rails/server_test.rb

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33

44
require "test_helper"
55
require "ruby_lsp/ruby_lsp_rails/server"
6+
require "timeout"
67

78
class ServerTest < ActiveSupport::TestCase
89
setup do
910
@stdout = StringIO.new
1011
@stderr = StringIO.new
12+
RubyLsp::Rails::ServerAddon.instance_variable_set(:@server_addon_classes, [])
13+
RubyLsp::Rails::ServerAddon.instance_variable_set(:@server_addons, {})
1114
@server = RubyLsp::Rails::Server.new(stdout: @stdout, stderr: @stderr, override_default_output_device: false)
1215
end
1316

@@ -268,10 +271,94 @@ def print_it!
268271
$> = original_stdout
269272
end
270273

274+
test "forked processes are named based on caller" do
275+
skip("Fork is not supported on Windows") if Gem.win_platform?
276+
277+
addon_path = File.expand_path("my_addon.rb")
278+
File.write(addon_path, <<~RUBY)
279+
class MyServerAddon < RubyLsp::Rails::ServerAddon
280+
def name
281+
"MyAddon"
282+
end
283+
284+
def execute(request, params)
285+
parent_process_title = `ps -p \#{Process.pid} -o comm=`.lines.last.strip
286+
file = "process_name.txt"
287+
pid = fork do
288+
# We can't directly send a message in these tests because we're using a StringIO as stdout instead of the
289+
# actual pipe, which means that the child process doesn't have access to the same object
290+
process_title = `ps -p \#{Process.pid} -o comm=`.lines.last.strip
291+
File.write(file, process_title)
292+
end
293+
294+
Process.wait(pid)
295+
296+
parent_process_title_changed = `ps -p \#{Process.pid} -o comm=`.lines.last.strip != parent_process_title
297+
send_message({ process_name: File.read(file), changed_parent_title: parent_process_title_changed })
298+
File.delete(file)
299+
rescue => e
300+
send_message({ error: e.full_message })
301+
end
302+
end
303+
RUBY
304+
305+
begin
306+
@server.execute("server_addon/register", server_addon_path: addon_path)
307+
@server.execute("server_addon/delegate", server_addon_name: "MyAddon", request_name: "dsl")
308+
assert_equal(response, { process_name: "ruby-lsp-rails: #{addon_path}", changed_parent_title: false })
309+
ensure
310+
FileUtils.rm(addon_path)
311+
end
312+
end
313+
314+
test "forked processes with no block are named based on caller" do
315+
skip("Fork is not supported on Windows") if Gem.win_platform?
316+
317+
addon_path = File.expand_path("my_other_addon.rb")
318+
File.write(addon_path, <<~RUBY)
319+
class MyOtherServerAddon < RubyLsp::Rails::ServerAddon
320+
def name
321+
"MyOtherAddon"
322+
end
323+
324+
def execute(request, params)
325+
parent_process_title = `ps -p \#{Process.pid} -o comm=`.lines.last.strip
326+
file = "other_process_name.txt"
327+
pid = fork
328+
329+
if pid
330+
Process.wait(pid)
331+
parent_process_title_changed = `ps -p \#{Process.pid} -o comm=`.lines.last.strip != parent_process_title
332+
send_message({ process_name: File.read(file), changed_parent_title: parent_process_title_changed })
333+
File.delete(file)
334+
else
335+
process_title = `ps -p \#{Process.pid} -o comm=`.lines.last.strip
336+
File.write(file, process_title)
337+
338+
# Exit from the child process or else we're stuck in the infinite loop of the server
339+
exit!
340+
end
341+
rescue => e
342+
send_message({ error: e.full_message })
343+
end
344+
end
345+
RUBY
346+
347+
begin
348+
@server.execute("server_addon/register", server_addon_path: addon_path)
349+
@server.execute("server_addon/delegate", server_addon_name: "MyOtherAddon", request_name: "dsl")
350+
assert_equal(response, { process_name: "ruby-lsp-rails: #{addon_path}", changed_parent_title: false })
351+
ensure
352+
FileUtils.rm(addon_path)
353+
end
354+
end
355+
271356
private
272357

273358
def response
274-
_headers, content = @stdout.string.split("\r\n\r\n")
275-
JSON.parse(content, symbolize_names: true)
359+
Timeout.timeout(2) do
360+
_headers, content = @stdout.string.split("\r\n\r\n")
361+
JSON.parse(content, symbolize_names: true)
362+
end
276363
end
277364
end

0 commit comments

Comments
 (0)