Skip to content

Commit c8fdb1a

Browse files
committed
Automatically name forked processes based on callers
1 parent 1b75ae3 commit c8fdb1a

File tree

2 files changed

+106
-0
lines changed

2 files changed

+106
-0
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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class ServerTest < ActiveSupport::TestCase
88
setup do
99
@stdout = StringIO.new
1010
@stderr = StringIO.new
11+
RubyLsp::Rails::ServerAddon.instance_variable_set(:@server_addon_classes, [])
12+
RubyLsp::Rails::ServerAddon.instance_variable_set(:@server_addons, {})
1113
@server = RubyLsp::Rails::Server.new(stdout: @stdout, stderr: @stderr, override_default_output_device: false)
1214
end
1315

@@ -268,6 +270,91 @@ def print_it!
268270
$> = original_stdout
269271
end
270272

273+
test "forked processes are named based on caller" do
274+
skip("Fork is not supported on Windows") if Gem.win_platform?
275+
276+
addon_path = File.expand_path("my_addon.rb")
277+
File.write(addon_path, <<~RUBY)
278+
module Patch
279+
def setproctitle(title)
280+
$TEST_TITLE = title
281+
end
282+
283+
Process.singleton_class.prepend(Patch)
284+
end
285+
286+
class MyServerAddon < RubyLsp::Rails::ServerAddon
287+
def name
288+
"MyAddon"
289+
end
290+
291+
def execute(request, params)
292+
file = "process_name.txt"
293+
294+
# We can't directly send a message in these tests because we're using a StringIO as stdout instead of the
295+
# actual pipe, which means that the child process doesn't have access to the same object
296+
pid = fork { File.write(file, $TEST_TITLE) }
297+
Process.wait(pid)
298+
299+
send_message({ process_name: File.read(file) })
300+
File.delete(file)
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}" })
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+
module Patch
320+
def setproctitle(title)
321+
$TEST_TITLE = title
322+
end
323+
324+
Process.singleton_class.prepend(Patch)
325+
end
326+
327+
class MyOtherServerAddon < RubyLsp::Rails::ServerAddon
328+
def name
329+
"MyOtherAddon"
330+
end
331+
332+
def execute(request, params)
333+
file = "other_process_name.txt"
334+
pid = fork
335+
336+
if pid
337+
Process.wait(pid)
338+
send_message({ process_name: File.read(file) })
339+
File.delete(file)
340+
else
341+
File.write(file, $TEST_TITLE)
342+
# Exit from the child process or else we're stuck in the infinite loop of the server
343+
exit!
344+
end
345+
end
346+
end
347+
RUBY
348+
349+
begin
350+
@server.execute("server_addon/register", server_addon_path: addon_path)
351+
@server.execute("server_addon/delegate", server_addon_name: "MyOtherAddon", request_name: "dsl")
352+
assert_equal(response, { process_name: "ruby-lsp-rails: #{addon_path}" })
353+
ensure
354+
FileUtils.rm(addon_path)
355+
end
356+
end
357+
271358
private
272359

273360
def response

0 commit comments

Comments
 (0)