Skip to content

Commit 7bb2c83

Browse files
Enhance process manager with universal bundler context fallback
This commit generalizes the foreman-specific bundler context fix to work for ALL process managers (overmind, foreman, and any future additions). Key improvements: - Context-aware process detection for all processes, not just foreman - Universal fallback mechanism using Bundler.with_unbundled_env - Consistent API with run_process(process, args) for any process manager - Enhanced test coverage for generalized functionality Benefits: - Robust handling of bundler context for any process manager - Future-proof architecture for new process managers - Consistent behavior across all system commands - Eliminates bundler interceptor issues universally Builds on previous commit to create a comprehensive solution. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 193a7d1 commit 7bb2c83

File tree

2 files changed

+102
-71
lines changed

2 files changed

+102
-71
lines changed

lib/react_on_rails/dev/process_manager.rb

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ module ReactOnRails
44
module Dev
55
class ProcessManager
66
class << self
7+
# Check if a process is available and usable in the current execution context
8+
# This accounts for bundler context where system commands might be intercepted
79
def installed?(process)
8-
IO.popen([process, "-v"], &:close)
9-
true
10-
rescue Errno::ENOENT
11-
false
10+
installed_in_current_context?(process)
1211
end
1312

1413
def ensure_procfile(procfile)
@@ -31,10 +30,10 @@ def run_with_process_manager(procfile)
3130
# Clean up stale files before starting
3231
FileManager.cleanup_stale_files
3332

34-
if installed?("overmind")
35-
system("overmind", "start", "-f", procfile)
36-
elsif foreman_available?
37-
run_foreman(procfile)
33+
if process_available?("overmind")
34+
run_process("overmind", ["start", "-f", procfile])
35+
elsif process_available?("foreman")
36+
run_process("foreman", ["start", "-f", procfile])
3837
else
3938
show_process_manager_installation_help
4039
exit 1
@@ -43,50 +42,63 @@ def run_with_process_manager(procfile)
4342

4443
private
4544

46-
# Check if foreman is available in either bundler context or system-wide
47-
def foreman_available?
48-
installed?("foreman") || foreman_available_in_system?
45+
# Check if a process is actually usable in the current execution context
46+
# This is important for commands that might be intercepted by bundler
47+
def installed_in_current_context?(process)
48+
# Try to execute the process with a simple flag to see if it works
49+
# Use system() because that's how we'll actually call it later
50+
system(process, "--version", out: File::NULL, err: File::NULL)
51+
rescue Errno::ENOENT
52+
false
53+
end
54+
55+
# Check if a process is available in either current context or system-wide
56+
def process_available?(process)
57+
installed?(process) || process_available_in_system?(process)
4958
end
5059

51-
# Try to run foreman with intelligent fallback strategy
52-
# First attempt: within bundler context (for projects that include foreman in Gemfile)
53-
# Fallback: outside bundler context (for projects following React on Rails best practices)
54-
def run_foreman(procfile)
55-
success = if installed?("foreman")
56-
# Try within bundle context first
57-
system("foreman", "start", "-f", procfile)
60+
# Try to run a process with intelligent fallback strategy
61+
# First attempt: within current context (for processes that are in the current bundle)
62+
# Fallback: outside bundler context (for system-installed processes)
63+
def run_process(process, args)
64+
success = if installed?(process)
65+
# Process works in current context - use it directly
66+
system(process, *args)
5867
else
5968
false
6069
end
6170

62-
# If bundler context failed or foreman not in bundle, try system foreman
71+
# If current context failed or process not available, try system process
6372
return if success
6473

65-
run_foreman_outside_bundle(procfile)
74+
run_process_outside_bundle(process, args)
6675
end
6776

68-
# Run foreman outside of bundler context using Bundler.with_unbundled_env
69-
# This allows using system-installed foreman even when it's not in the Gemfile
70-
def run_foreman_outside_bundle(procfile)
77+
# Run a process outside of bundler context using Bundler.with_unbundled_env
78+
# This allows using system-installed processes even when they're not in the Gemfile
79+
def run_process_outside_bundle(process, args)
7180
if defined?(Bundler)
7281
Bundler.with_unbundled_env do
73-
system("foreman", "start", "-f", procfile)
82+
system(process, *args)
7483
end
7584
else
7685
# Fallback if Bundler is not available
77-
system("foreman", "start", "-f", procfile)
86+
system(process, *args)
7887
end
7988
end
8089

81-
# Check if foreman is available system-wide (outside bundle context)
82-
def foreman_available_in_system?
90+
# Check if a process is available system-wide (outside bundle context)
91+
def process_available_in_system?(process)
8392
if defined?(Bundler)
8493
Bundler.with_unbundled_env do
85-
installed?("foreman")
94+
# Use system() directly to check if process exists outside bundler context
95+
system(process, "--version", out: File::NULL, err: File::NULL)
8696
end
8797
else
8898
false
8999
end
100+
rescue Errno::ENOENT
101+
false
90102
end
91103

92104
# Improved error message with helpful guidance

spec/react_on_rails/dev/process_manager_spec.rb

Lines changed: 62 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,23 @@
1818
end
1919

2020
describe ".installed?" do
21-
it "returns true when process is available" do
22-
allow(IO).to receive(:popen).with(["overmind", "-v"]).and_return("Some version info")
21+
it "returns true when process is available in current context" do
22+
expect_any_instance_of(Kernel).to receive(:system)
23+
.with("overmind", "--version", out: File::NULL, err: File::NULL).and_return(true)
2324
expect(described_class).to be_installed("overmind")
2425
end
2526

26-
it "returns false when process is not available" do
27-
allow(IO).to receive(:popen).with(["nonexistent", "-v"]).and_raise(Errno::ENOENT)
27+
it "returns false when process is not available in current context" do
28+
expect_any_instance_of(Kernel).to receive(:system)
29+
.with("nonexistent", "--version", out: File::NULL, err: File::NULL).and_raise(Errno::ENOENT)
2830
expect(described_class.installed?("nonexistent")).to be false
2931
end
32+
33+
it "returns false when process returns false" do
34+
expect_any_instance_of(Kernel).to receive(:system)
35+
.with("failing_process", "--version", out: File::NULL, err: File::NULL).and_return(false)
36+
expect(described_class.installed?("failing_process")).to be false
37+
end
3038
end
3139

3240
describe ".ensure_procfile" do
@@ -50,123 +58,134 @@
5058
end
5159

5260
it "uses overmind when available" do
53-
allow(described_class).to receive(:installed?).with("overmind").and_return(true)
54-
expect_any_instance_of(Kernel).to receive(:system).with("overmind", "start", "-f", "Procfile.dev")
61+
allow(described_class).to receive(:process_available?).with("overmind").and_return(true)
62+
expect(described_class).to receive(:run_process).with("overmind", ["start", "-f", "Procfile.dev"])
5563

5664
described_class.run_with_process_manager("Procfile.dev")
5765
end
5866

59-
it "uses foreman when overmind not available and foreman is in bundle" do
60-
allow(described_class).to receive(:installed?).with("overmind").and_return(false)
61-
allow(described_class).to receive(:foreman_available?).and_return(true)
62-
allow(described_class).to receive(:installed?).with("foreman").and_return(true)
63-
expect(described_class).to receive(:run_foreman).with("Procfile.dev")
67+
it "uses foreman when overmind not available and foreman is available" do
68+
allow(described_class).to receive(:process_available?).with("overmind").and_return(false)
69+
allow(described_class).to receive(:process_available?).with("foreman").and_return(true)
70+
expect(described_class).to receive(:run_process).with("foreman", ["start", "-f", "Procfile.dev"])
6471

6572
described_class.run_with_process_manager("Procfile.dev")
6673
end
6774

6875
it "exits with error when no process manager available" do
69-
allow(described_class).to receive(:installed?).with("overmind").and_return(false)
70-
allow(described_class).to receive(:foreman_available?).and_return(false)
76+
allow(described_class).to receive(:process_available?).with("overmind").and_return(false)
77+
allow(described_class).to receive(:process_available?).with("foreman").and_return(false)
7178
expect(described_class).to receive(:show_process_manager_installation_help)
7279
expect_any_instance_of(Kernel).to receive(:exit).with(1)
7380

7481
described_class.run_with_process_manager("Procfile.dev")
7582
end
7683

7784
it "cleans up stale files before starting" do
78-
allow(described_class).to receive(:installed?).with("overmind").and_return(true)
85+
allow(described_class).to receive(:process_available?).with("overmind").and_return(true)
86+
allow(described_class).to receive(:run_process)
7987
expect(ReactOnRails::Dev::FileManager).to receive(:cleanup_stale_files)
8088

8189
described_class.run_with_process_manager("Procfile.dev")
8290
end
8391
end
8492

85-
describe ".foreman_available?" do
86-
it "returns true when foreman is available in bundle context" do
93+
describe ".process_available?" do
94+
it "returns true when process is available in current context" do
8795
allow(described_class).to receive(:installed?).with("foreman").and_return(true)
88-
allow(described_class).to receive(:foreman_available_in_system?).and_return(false)
96+
allow(described_class).to receive(:process_available_in_system?).with("foreman").and_return(false)
8997

90-
expect(described_class.send(:foreman_available?)).to be true
98+
expect(described_class.send(:process_available?, "foreman")).to be true
9199
end
92100

93-
it "returns true when foreman is available system-wide" do
101+
it "returns true when process is available system-wide" do
94102
allow(described_class).to receive(:installed?).with("foreman").and_return(false)
95-
allow(described_class).to receive(:foreman_available_in_system?).and_return(true)
103+
allow(described_class).to receive(:process_available_in_system?).with("foreman").and_return(true)
96104

97-
expect(described_class.send(:foreman_available?)).to be true
105+
expect(described_class.send(:process_available?, "foreman")).to be true
98106
end
99107

100-
it "returns false when foreman is not available anywhere" do
108+
it "returns false when process is not available anywhere" do
101109
allow(described_class).to receive(:installed?).with("foreman").and_return(false)
102-
allow(described_class).to receive(:foreman_available_in_system?).and_return(false)
110+
allow(described_class).to receive(:process_available_in_system?).with("foreman").and_return(false)
103111

104-
expect(described_class.send(:foreman_available?)).to be false
112+
expect(described_class.send(:process_available?, "foreman")).to be false
105113
end
106114
end
107115

108-
describe ".run_foreman" do
116+
describe ".run_process" do
109117
before do
110118
allow_any_instance_of(Kernel).to receive(:system).and_return(true)
111119
end
112120

113-
it "tries bundle context first when foreman is in bundle" do
121+
it "tries current context first when process works there" do
114122
allow(described_class).to receive(:installed?).with("foreman").and_return(true)
115123
expect_any_instance_of(Kernel).to receive(:system).with("foreman", "start", "-f", "Procfile.dev").and_return(true)
116-
expect(described_class).not_to receive(:run_foreman_outside_bundle)
124+
expect(described_class).not_to receive(:run_process_outside_bundle)
117125

118-
described_class.send(:run_foreman, "Procfile.dev")
126+
described_class.send(:run_process, "foreman", ["start", "-f", "Procfile.dev"])
119127
end
120128

121-
it "falls back to system foreman when bundle context fails" do
129+
it "falls back to system process when current context fails" do
122130
allow(described_class).to receive(:installed?).with("foreman").and_return(true)
123131
expect_any_instance_of(Kernel).to receive(:system)
124132
.with("foreman", "start", "-f", "Procfile.dev").and_return(false)
125-
expect(described_class).to receive(:run_foreman_outside_bundle).with("Procfile.dev")
133+
expect(described_class).to receive(:run_process_outside_bundle).with("foreman", ["start", "-f", "Procfile.dev"])
126134

127-
described_class.send(:run_foreman, "Procfile.dev")
135+
described_class.send(:run_process, "foreman", ["start", "-f", "Procfile.dev"])
128136
end
129137

130-
it "uses system foreman directly when not in bundle" do
131-
allow(described_class).to receive(:installed?).with("foreman").and_return(false)
132-
expect(described_class).to receive(:run_foreman_outside_bundle).with("Procfile.dev")
138+
it "uses system process directly when not available in current context" do
139+
allow(described_class).to receive(:installed?).with("overmind").and_return(false)
140+
expect(described_class).to receive(:run_process_outside_bundle).with("overmind", ["start", "-f", "Procfile.dev"])
133141

134-
described_class.send(:run_foreman, "Procfile.dev")
142+
described_class.send(:run_process, "overmind", ["start", "-f", "Procfile.dev"])
135143
end
136144
end
137145

138-
describe ".run_foreman_outside_bundle" do
146+
describe ".run_process_outside_bundle" do
139147
it "uses Bundler.with_unbundled_env when Bundler is available" do
140148
bundler_double = class_double(Bundler)
141149
stub_const("Bundler", bundler_double)
142150
expect(bundler_double).to receive(:with_unbundled_env).and_yield
143151
expect_any_instance_of(Kernel).to receive(:system).with("foreman", "start", "-f", "Procfile.dev")
144152

145-
described_class.send(:run_foreman_outside_bundle, "Procfile.dev")
153+
described_class.send(:run_process_outside_bundle, "foreman", ["start", "-f", "Procfile.dev"])
146154
end
147155

148156
it "falls back to direct system call when Bundler is not available" do
149157
hide_const("Bundler")
150158
expect_any_instance_of(Kernel).to receive(:system).with("foreman", "start", "-f", "Procfile.dev")
151159

152-
described_class.send(:run_foreman_outside_bundle, "Procfile.dev")
160+
described_class.send(:run_process_outside_bundle, "foreman", ["start", "-f", "Procfile.dev"])
153161
end
154162
end
155163

156-
describe ".foreman_available_in_system?" do
157-
it "checks foreman availability outside bundle context" do
164+
describe ".process_available_in_system?" do
165+
it "checks process availability outside bundle context" do
158166
bundler_double = class_double(Bundler)
159167
stub_const("Bundler", bundler_double)
160168
expect(bundler_double).to receive(:with_unbundled_env).and_yield
161-
expect(described_class).to receive(:installed?).with("foreman").and_return(true)
169+
expect_any_instance_of(Kernel).to receive(:system)
170+
.with("foreman", "--version", out: File::NULL, err: File::NULL).and_return(true)
162171

163-
expect(described_class.send(:foreman_available_in_system?)).to be true
172+
expect(described_class.send(:process_available_in_system?, "foreman")).to be true
164173
end
165174

166175
it "returns false when Bundler is not available" do
167176
hide_const("Bundler")
168177

169-
expect(described_class.send(:foreman_available_in_system?)).to be false
178+
expect(described_class.send(:process_available_in_system?, "foreman")).to be false
179+
end
180+
181+
it "returns false when process fails outside bundle context" do
182+
bundler_double = class_double(Bundler)
183+
stub_const("Bundler", bundler_double)
184+
expect(bundler_double).to receive(:with_unbundled_env).and_yield
185+
expect_any_instance_of(Kernel).to receive(:system)
186+
.with("overmind", "--version", out: File::NULL, err: File::NULL).and_return(false)
187+
188+
expect(described_class.send(:process_available_in_system?, "overmind")).to be false
170189
end
171190
end
172191

0 commit comments

Comments
 (0)