rails_accessibility_testing 1.4.2 → 1.5.0

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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +212 -53
  3. data/CHANGELOG.md +135 -0
  4. data/GUIDES/getting_started.md +171 -9
  5. data/GUIDES/system_specs_for_accessibility.md +13 -12
  6. data/README.md +139 -36
  7. data/docs_site/getting_started.md +142 -18
  8. data/docs_site/index.md +1 -1
  9. data/exe/a11y_live_scanner +361 -0
  10. data/exe/rails_server_safe +18 -1
  11. data/lib/generators/rails_a11y/install/install_generator.rb +137 -0
  12. data/lib/rails_accessibility_testing/accessibility_helper.rb +547 -24
  13. data/lib/rails_accessibility_testing/change_detector.rb +17 -104
  14. data/lib/rails_accessibility_testing/checks/base_check.rb +56 -7
  15. data/lib/rails_accessibility_testing/checks/heading_check.rb +138 -0
  16. data/lib/rails_accessibility_testing/checks/image_alt_text_check.rb +7 -7
  17. data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +11 -1
  18. data/lib/rails_accessibility_testing/cli/command.rb +3 -1
  19. data/lib/rails_accessibility_testing/config/yaml_loader.rb +1 -1
  20. data/lib/rails_accessibility_testing/engine/rule_engine.rb +49 -5
  21. data/lib/rails_accessibility_testing/error_message_builder.rb +63 -7
  22. data/lib/rails_accessibility_testing/middleware/page_visit_logger.rb +81 -0
  23. data/lib/rails_accessibility_testing/railtie.rb +22 -0
  24. data/lib/rails_accessibility_testing/rspec_integration.rb +176 -10
  25. data/lib/rails_accessibility_testing/version.rb +1 -1
  26. data/lib/rails_accessibility_testing.rb +8 -3
  27. metadata +8 -4
  28. data/lib/generators/rails_a11y/install/generator.rb +0 -51
  29. data/lib/rails_accessibility_testing/checks/heading_hierarchy_check.rb +0 -53
@@ -20,10 +20,18 @@ Add to your `Gemfile`:
20
20
  ```ruby
21
21
  group :development, :test do
22
22
  gem 'rails_accessibility_testing'
23
+ gem 'rspec-rails', '~> 8.0' # Required for system specs
23
24
  gem 'axe-core-capybara', '~> 4.0'
25
+ gem 'capybara', '~> 3.40'
26
+ gem 'selenium-webdriver', '~> 4.0'
27
+ gem 'webdrivers', '~> 5.0' # Optional but recommended for automatic driver management
24
28
  end
25
29
  ```
26
30
 
31
+ **Important:**
32
+ - You must explicitly add `selenium-webdriver` to your Gemfile. It's not automatically included as a dependency.
33
+ - **RSpec Rails is required** - The generator creates system specs that require `rspec-rails`. If you're using Minitest, you'll need to manually create your accessibility tests.
34
+
27
35
  Then run:
28
36
 
29
37
  ```bash
@@ -41,17 +49,22 @@ rails generate rails_a11y:install
41
49
  This creates:
42
50
  - `config/initializers/rails_a11y.rb` - Configuration
43
51
  - `config/accessibility.yml` - Check settings
52
+ - `spec/system/all_pages_accessibility_spec.rb` - Comprehensive spec that tests all GET routes
44
53
  - Updates `spec/rails_helper.rb` (if using RSpec)
45
54
 
46
- ### Step 3: Create System Specs (Recommended)
55
+ **Note:** If you already have system tests set up in your Rails application, you can skip to Step 3. If you need help configuring Capybara or installing Chrome, see the [Troubleshooting section](#troubleshooting) below.
56
+
57
+ ### Step 3: Run Your Tests
58
+
59
+ The generator creates `spec/system/all_pages_accessibility_spec.rb` which automatically tests all GET routes in your application.
47
60
 
48
- Create system specs for the pages you want to test. This is the **recommended and most reliable** approach:
61
+ You can also create custom system specs for specific pages:
49
62
 
50
63
  ```ruby
51
- # spec/system/home_page_accessibility_spec.rb
64
+ # spec/system/my_page_accessibility_spec.rb
52
65
  require 'rails_helper'
53
66
 
54
- RSpec.describe 'Home Page Accessibility', type: :system do
67
+ RSpec.describe 'My Page Accessibility', type: :system do
55
68
  it 'loads the page and runs comprehensive accessibility checks' do
56
69
  visit root_path
57
70
 
@@ -63,40 +76,151 @@ RSpec.describe 'Home Page Accessibility', type: :system do
63
76
  end
64
77
  ```
65
78
 
66
- ### Step 4: Run Your Tests
79
+ ### Step 5: Run Your Tests
67
80
 
68
81
  You can run accessibility checks in several ways:
69
82
 
70
83
  #### Option A: Run Tests Manually
71
84
 
72
85
  ```bash
86
+ # Run all accessibility specs
87
+ bundle exec rspec spec/system/*_accessibility_spec.rb
88
+
89
+ # Or run all system specs
73
90
  bundle exec rspec spec/system/
74
91
  ```
75
92
 
76
93
  Accessibility checks run automatically on every system test that visits a page.
77
94
 
78
- #### Option B: Run Continuously with Procfile (Recommended for Development)
79
95
 
80
- For continuous accessibility checking during development, add to your `Procfile.dev`:
96
+ ## Troubleshooting
97
+
98
+ ### How do I configure Capybara for system tests?
99
+
100
+ If you don't already have system tests configured, you need to set up Capybara with a Selenium driver. Create `spec/support/driver.rb`:
101
+
102
+ ```ruby
103
+ # spec/support/driver.rb
104
+ require 'selenium-webdriver'
105
+ require 'capybara/rails'
106
+ require 'capybara/rspec'
107
+
108
+ # Configure Chrome options
109
+ browser_options = Selenium::WebDriver::Chrome::Options.new
110
+ browser_options.add_argument('--window-size=1920,1080')
111
+ browser_options.add_argument('--headless') unless ENV['SHOW_TEST_BROWSER']
112
+
113
+ # Register the driver
114
+ Capybara.register_driver :selenium_chrome_headless do |app|
115
+ Capybara::Selenium::Driver.new(
116
+ app,
117
+ browser: :chrome,
118
+ options: browser_options
119
+ )
120
+ end
121
+
122
+ # Set as default JavaScript driver
123
+ Capybara.javascript_driver = :selenium_chrome_headless
81
124
 
82
- ```procfile
83
- web: bin/rails server
84
- css: bin/rails dartsass:watch
85
- a11y: while true; do bundle exec rspec spec/system/*_accessibility_spec.rb; sleep 30; done
125
+ # Configure RSpec to use the driver for system tests
126
+ RSpec.configure do |config|
127
+ config.before(:each, type: :system) do
128
+ driven_by :selenium_chrome_headless
129
+ end
130
+ end
86
131
  ```
87
132
 
88
- Then run:
133
+ **Note for Rails 8:** Rails 8 uses `driven_by` to configure system tests. Make sure your `spec/support/driver.rb` is loaded by `rails_helper.rb` (it should be automatically loaded if it's in the `spec/support/` directory).
134
+
135
+ ### How do I install Chrome/Chromium?
136
+
137
+ System tests require Chrome or Chromium to be installed on your system:
138
+
139
+ **macOS:**
140
+ ```bash
141
+ brew install --cask google-chrome
142
+ # or for Chromium:
143
+ brew install --cask chromium
144
+ ```
89
145
 
146
+ **Linux (Ubuntu/Debian):**
90
147
  ```bash
91
- bin/dev
148
+ sudo apt-get update
149
+ sudo apt-get install -y google-chrome-stable
150
+ # or for Chromium:
151
+ sudo apt-get install -y chromium-browser
92
152
  ```
93
153
 
94
- This will:
95
- - Start your Rails server
96
- - Watch for CSS changes
97
- - **Automatically run accessibility checks every 30 seconds** on all `*_accessibility_spec.rb` files
154
+ **Windows:**
155
+ Download and install Chrome from [google.com/chrome](https://www.google.com/chrome/)
156
+
157
+ The `webdrivers` gem will automatically download and manage the ChromeDriver binary for you.
158
+
159
+ ### Error: `uninitialized constant Selenium::WebDriver::DriverFinder`
160
+
161
+ This error typically occurs when:
162
+ 1. **Missing selenium-webdriver gem** - Make sure you've added `gem 'selenium-webdriver', '~> 4.0'` to your Gemfile
163
+ 2. **Version incompatibility** - Ensure you're using compatible versions:
164
+ - `selenium-webdriver` ~> 4.0 (4.6.0+ recommended for Rails 8)
165
+ - `webdrivers` ~> 5.0 (if using webdrivers)
166
+ - `capybara` ~> 3.40
167
+
168
+ **Solution:**
169
+ ```bash
170
+ # Update your Gemfile
171
+ gem 'selenium-webdriver', '~> 4.10'
172
+ gem 'webdrivers', '~> 5.3'
173
+ gem 'capybara', '~> 3.40'
174
+
175
+ # Then run
176
+ bundle update selenium-webdriver webdrivers capybara
177
+ ```
178
+
179
+ ### Error: Chrome/ChromeDriver not found
180
+
181
+ **Solution:**
182
+ 1. Make sure Chrome is installed (see Step 2.5 above)
183
+ 2. If using `webdrivers` gem, it should auto-download ChromeDriver. If not:
184
+ ```bash
185
+ bundle exec webdrivers chrome
186
+ ```
187
+ 3. For manual installation, download from [ChromeDriver downloads](https://chromedriver.chromium.org/downloads)
188
+
189
+ ### System tests not running
190
+
191
+ **Check:**
192
+ 1. Your spec has `type: :system` metadata
193
+ 2. `spec/support/driver.rb` exists and is properly configured
194
+ 3. `spec/rails_helper.rb` loads support files (should be automatic)
195
+ 4. Chrome is installed and accessible
196
+
197
+ ### Tests are slow
198
+
199
+ Disable expensive checks in development:
200
+ ```yaml
201
+ # config/accessibility.yml
202
+ development:
203
+ checks:
204
+ color_contrast: false # Disable expensive color contrast checks
205
+ ```
206
+
207
+ ## Version Compatibility
208
+
209
+ For best results, use these compatible versions:
210
+
211
+ | Component | Recommended Version | Minimum Version | Required |
212
+ |-----------|-------------------|-----------------|----------|
213
+ | Ruby | 3.1+ | 3.0+ | Yes |
214
+ | Rails | 7.1+ / 8.0+ | 6.0+ | Yes |
215
+ | **RSpec Rails** | **8.0+** | **6.0+** | **Yes (for system specs)** |
216
+ | Capybara | ~> 3.40 | 3.0+ | Yes |
217
+ | selenium-webdriver | ~> 4.10 | 4.0+ | Yes |
218
+ | webdrivers | ~> 5.3 | 5.0+ | Optional |
98
219
 
99
- The accessibility checker will continuously monitor your pages and alert you to any issues as you develop!
220
+ **Rails 8 Notes:**
221
+ - Rails 8 requires `selenium-webdriver` 4.6.0+ for `DriverFinder` support
222
+ - Make sure your `driven_by` configuration is in `spec/support/driver.rb`
223
+ - Rails 8 system tests use `driven_by` instead of direct Capybara configuration
100
224
 
101
225
  ## Learn More
102
226
 
data/docs_site/index.md CHANGED
@@ -7,7 +7,7 @@ title: Home
7
7
 
8
8
  **The RSpec + RuboCop of accessibility for Rails. Catch WCAG violations before they reach production.**
9
9
 
10
- **Version:** 1.4.2
10
+ **Version:** 1.4.3
11
11
 
12
12
  Rails Accessibility Testing is a comprehensive accessibility testing gem that makes accessibility testing as natural as unit testing. It integrates seamlessly into your Rails workflow, catching WCAG 2.1 AA violations as you code—not after deployment.
13
13
 
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Live accessibility scanner for development
5
+ # Monitors page visits and runs accessibility checks automatically
6
+ # Usage: Add to Procfile.dev as: a11y: bundle exec a11y_live_scanner
7
+
8
+ require 'bundler/setup'
9
+ require 'json'
10
+ require 'fileutils'
11
+ require 'timeout'
12
+
13
+ # Load Rails environment
14
+ # Find Rails root by looking for config/environment.rb
15
+ rails_root = Dir.pwd
16
+ while rails_root != '/' && !File.exist?(File.join(rails_root, 'config', 'environment.rb'))
17
+ rails_root = File.dirname(rails_root)
18
+ end
19
+
20
+ if File.exist?(File.join(rails_root, 'config', 'environment.rb'))
21
+ Dir.chdir(rails_root)
22
+ require File.join(rails_root, 'config', 'environment')
23
+ end
24
+
25
+ # Require the gem and ensure all components are loaded
26
+ begin
27
+ require 'rails_accessibility_testing'
28
+ # Verify AccessibilityHelper is available
29
+ unless defined?(RailsAccessibilityTesting::AccessibilityHelper)
30
+ raise LoadError, "RailsAccessibilityTesting::AccessibilityHelper not found after requiring gem"
31
+ end
32
+ rescue LoadError => e
33
+ $stderr.puts "❌ Error loading rails_accessibility_testing gem: #{e.message}"
34
+ $stderr.puts " Make sure the gem is installed: bundle install"
35
+ exit 1
36
+ end
37
+
38
+ require 'capybara'
39
+ require 'capybara/dsl'
40
+ require 'selenium-webdriver'
41
+
42
+ # Try to require webdrivers
43
+ begin
44
+ require 'webdrivers'
45
+ rescue LoadError
46
+ # webdrivers not available
47
+ end
48
+
49
+ # Setup Capybara for live scanning
50
+ Capybara.default_driver = :selenium_chrome_headless
51
+ Capybara.app_host = ENV.fetch('RAILS_URL', 'http://localhost:3000')
52
+ Capybara.default_max_wait_time = 5
53
+
54
+ # Configure Chrome options
55
+ browser_options = Selenium::WebDriver::Chrome::Options.new
56
+ browser_options.add_argument('--headless')
57
+ browser_options.add_argument('--window-size=1920,1080')
58
+ browser_options.add_argument('--disable-gpu')
59
+ browser_options.add_argument('--no-sandbox')
60
+
61
+ Capybara.register_driver :selenium_chrome_headless do |app|
62
+ Capybara::Selenium::Driver.new(
63
+ app,
64
+ browser: :chrome,
65
+ options: browser_options
66
+ )
67
+ end
68
+
69
+ include Capybara::DSL
70
+
71
+ # Page visit log file
72
+ def log_file
73
+ defined?(Rails) ? File.join(Rails.root, 'tmp', 'a11y_page_visits.log') : File.join(Dir.pwd, 'tmp', 'a11y_page_visits.log')
74
+ end
75
+
76
+ def scanned_pages_file
77
+ defined?(Rails) ? File.join(Rails.root, 'tmp', 'a11y_scanned_pages.json') : File.join(Dir.pwd, 'tmp', 'a11y_scanned_pages.json')
78
+ end
79
+
80
+ # Track which pages we've already scanned (to avoid duplicates)
81
+ def load_scanned_pages
82
+ return {} unless File.exist?(scanned_pages_file)
83
+ JSON.parse(File.read(scanned_pages_file))
84
+ rescue StandardError
85
+ {}
86
+ end
87
+
88
+ def save_scanned_page(path, timestamp)
89
+ scanned = load_scanned_pages
90
+ scanned[path] = timestamp
91
+ # Use atomic write to avoid triggering file watchers unnecessarily
92
+ temp_file = "#{scanned_pages_file}.tmp"
93
+ File.write(temp_file, scanned.to_json)
94
+ FileUtils.mv(temp_file, scanned_pages_file)
95
+ rescue StandardError => e
96
+ # Silently fail - don't break scanning if file write fails
97
+ end
98
+
99
+ def page_already_scanned?(path, visit_timestamp)
100
+ scanned = load_scanned_pages
101
+ return false unless scanned[path]
102
+ # Don't rescan if scanned within last 30 seconds (debounce)
103
+ last_scan_time = scanned[path].to_f
104
+ (Time.now.to_f - last_scan_time) < 30
105
+ end
106
+
107
+ # Run accessibility check on a page
108
+ def check_page(path, url = nil)
109
+ begin
110
+ # Check if scan was cancelled before starting
111
+ if $scan_cancelled
112
+ puts "⏭️ Scan cancelled - user navigated to different page"
113
+ return nil
114
+ end
115
+
116
+ # Show what we're about to scan
117
+ puts "\n" + "="*70
118
+ puts "🔍 Starting accessibility scan..."
119
+ puts "="*70
120
+ puts "📍 Path: #{path}"
121
+ puts "🔗 URL: #{url || path}"
122
+ puts "⏰ Time: #{Time.now.strftime('%H:%M:%S')}"
123
+ puts "="*70
124
+ puts ""
125
+
126
+ # Visit the page
127
+ puts " 🌐 Loading page..."
128
+ visit path
129
+
130
+ # Check for cancellation after page load
131
+ if $scan_cancelled || $current_scan_target != path
132
+ puts "⏭️ Scan cancelled - user navigated to different page"
133
+ return nil
134
+ end
135
+
136
+ # Capybara already waits for page load, no need for explicit sleep
137
+ # Only wait if page hasn't loaded (Capybara handles this automatically)
138
+
139
+ # Get page details
140
+ current_url = page.current_url rescue path
141
+ page_title = page.title rescue 'Unknown'
142
+
143
+ # Skip if redirected to sign in
144
+ if current_url.include?('sign_in') || current_url.include?('login')
145
+ puts "⏭️ Skipping #{path}: requires authentication"
146
+ puts " Redirected to: #{current_url}"
147
+ return nil
148
+ end
149
+
150
+ # Check for cancellation before running checks
151
+ if $scan_cancelled || $current_scan_target != path
152
+ puts "⏭️ Scan cancelled - user navigated to different page"
153
+ return nil
154
+ end
155
+
156
+ # Show page details
157
+ puts " ✓ Page loaded successfully"
158
+ puts " 📄 Title: #{page_title}"
159
+ puts " 🔗 Final URL: #{current_url}"
160
+ puts ""
161
+ puts " 🔍 Running accessibility checks..."
162
+ puts ""
163
+
164
+ # Run accessibility checks using Capybara page directly
165
+ # Verify AccessibilityHelper is available
166
+ unless defined?(RailsAccessibilityTesting::AccessibilityHelper)
167
+ raise "RailsAccessibilityTesting::AccessibilityHelper is not available. Make sure the gem is properly installed."
168
+ end
169
+
170
+ # Create a minimal helper instance
171
+ helper_class = Class.new do
172
+ include RailsAccessibilityTesting::AccessibilityHelper
173
+
174
+ def initialize(page)
175
+ @page = page
176
+ end
177
+
178
+ def page
179
+ @page
180
+ end
181
+ end
182
+
183
+ helper = helper_class.new(page)
184
+
185
+ # Run checks with periodic cancellation checks
186
+ # Note: The checks themselves don't support cancellation yet, but we check before/after
187
+ result = helper.check_comprehensive_accessibility
188
+
189
+ # Check for cancellation after checks complete
190
+ if $scan_cancelled || $current_scan_target != path
191
+ puts "⏭️ Scan cancelled - user navigated to different page"
192
+ return nil
193
+ end
194
+
195
+ # Show scan completion
196
+ puts ""
197
+ puts "="*70
198
+ puts "✅ Scan completed for: #{path}"
199
+ if result && result.is_a?(Hash)
200
+ puts " Errors: #{result[:errors] || 0}, Warnings: #{result[:warnings] || 0}"
201
+ end
202
+ puts "="*70
203
+ puts ""
204
+
205
+ # Return result
206
+ result
207
+ rescue StandardError => e
208
+ # Don't show error if scan was cancelled
209
+ if $scan_cancelled || $current_scan_target != path
210
+ return nil
211
+ end
212
+
213
+ puts ""
214
+ puts "="*70
215
+ puts "⚠️ Error checking #{path}"
216
+ puts " Error: #{e.message}"
217
+ puts " URL: #{url || path}"
218
+ puts "="*70
219
+ puts ""
220
+ nil
221
+ end
222
+ end
223
+
224
+ # Global variable to track current scan target (for cancellation)
225
+ $current_scan_target = nil
226
+ $scan_cancelled = false
227
+
228
+ # Main scanner loop
229
+ def run_scanner
230
+ puts "\n" + "="*70
231
+ puts "🔍 Live Accessibility Scanner Started"
232
+ puts "="*70
233
+ puts "📍 Monitoring: #{Capybara.app_host}"
234
+ puts "📝 Watching: #{log_file}"
235
+ puts "💡 Navigate to pages in your browser to see accessibility reports"
236
+ puts "⏱️ Checking for new page visits every 2 seconds..."
237
+ puts "💡 Tip: If you navigate to a new page, current scan will be cancelled"
238
+ puts "="*70
239
+ puts ""
240
+ puts "⏳ Waiting for page visits... (scanning will start automatically)"
241
+ puts ""
242
+
243
+ # Create log file if it doesn't exist
244
+ FileUtils.mkdir_p(File.dirname(log_file))
245
+ FileUtils.touch(log_file) unless File.exist?(log_file)
246
+
247
+ # Start reading from end of file
248
+ last_position = File.exist?(log_file) ? File.size(log_file) : 0
249
+ last_scan_time = {} # In-memory debounce cache
250
+
251
+ loop do
252
+ begin
253
+ # Check if log file has new content
254
+ current_size = File.exist?(log_file) ? File.size(log_file) : 0
255
+
256
+ if current_size > last_position
257
+ # Read all new lines to get the latest page visit
258
+ new_visits = []
259
+ File.open(log_file, 'r') do |f|
260
+ f.seek(last_position)
261
+ f.each_line do |line|
262
+ next if line.strip.empty?
263
+
264
+ begin
265
+ visit_data = JSON.parse(line.strip)
266
+ new_visits << visit_data
267
+ rescue JSON::ParserError => e
268
+ # Skip invalid JSON
269
+ next
270
+ end
271
+ end
272
+ end
273
+
274
+ # Process only the most recent visit (user's current page)
275
+ if new_visits.any?
276
+ begin
277
+ # Get the latest visit (last in the array)
278
+ latest_visit = new_visits.last
279
+ path = latest_visit['path']
280
+ url = latest_visit['url']
281
+ timestamp = latest_visit['timestamp'].to_f
282
+
283
+ # Debounce: Skip if we scanned this page in the last 30 seconds (in-memory check)
284
+ if last_scan_time[path] && (Time.now.to_f - last_scan_time[path]) < 30
285
+ last_position = current_size
286
+ next
287
+ end
288
+
289
+ # Skip if we've already scanned this page (from persistent storage)
290
+ if page_already_scanned?(path, timestamp)
291
+ last_position = current_size
292
+ next
293
+ end
294
+
295
+ # Cancel current scan if scanning a different page
296
+ if $current_scan_target && $current_scan_target != path
297
+ puts "\n⚠️ User navigated to new page - cancelling current scan"
298
+ puts " Previous: #{$current_scan_target}"
299
+ puts " New: #{path}"
300
+ $scan_cancelled = true
301
+ # Give a moment for cancellation to be processed
302
+ sleep 0.5
303
+ end
304
+
305
+ # Reset cancellation flag and set new target
306
+ $scan_cancelled = false
307
+ $current_scan_target = path
308
+
309
+ # Mark as scanned (both in memory and on disk)
310
+ last_scan_time[path] = Time.now.to_f
311
+ save_scanned_page(path, timestamp)
312
+
313
+ # Show that we detected a new page visit
314
+ visit_time = Time.at(timestamp)
315
+ puts "\n" + "🎯" + "="*69
316
+ puts "📥 NEW PAGE VISIT DETECTED"
317
+ puts "="*70
318
+ puts " Path: #{path}"
319
+ puts " URL: #{url}"
320
+ puts " Visited at: #{visit_time.strftime('%H:%M:%S')}"
321
+ puts " Starting scan..."
322
+ puts "="*70
323
+
324
+ # Run check with URL info
325
+ check_page(path, url)
326
+
327
+ # Clear scan target after completion
328
+ $current_scan_target = nil if $current_scan_target == path
329
+ rescue StandardError => e
330
+ puts "⚠️ Error processing visit: #{e.message}"
331
+ end
332
+ end
333
+
334
+ last_position = current_size
335
+ end
336
+
337
+ # Sleep longer to reduce file system activity and avoid triggering Rails reloads
338
+ # Check every 2 seconds - fast enough for live scanning, slow enough to not overload
339
+ # Show a subtle indicator that we're still monitoring (every 10 seconds)
340
+ if Time.now.to_i % 10 == 0
341
+ print "💚 Monitoring... (#{Time.now.strftime('%H:%M:%S')})\r"
342
+ $stdout.flush
343
+ end
344
+ sleep 2
345
+
346
+ rescue StandardError => e
347
+ puts "\n⚠️ Scanner error: #{e.message}"
348
+ puts " Continuing to monitor..."
349
+ sleep 2
350
+ end
351
+ end
352
+ end
353
+
354
+ # Run the scanner
355
+ begin
356
+ run_scanner
357
+ rescue Interrupt
358
+ puts "\n\n👋 Live scanner stopped"
359
+ exit 0
360
+ end
361
+
@@ -15,7 +15,17 @@ if [ -f "$PIDFILE" ]; then
15
15
  # Check if it's listening on the expected port
16
16
  if lsof -ti:$PORT -sTCP:LISTEN | grep -q "^$PID$" 2>/dev/null; then
17
17
  echo "Server is already running (pid: $PID, port: $PORT)"
18
+ echo "Keeping process alive for Foreman..."
19
+ # Keep the process alive by waiting indefinitely
20
+ # This prevents Foreman from shutting down all processes
21
+ while true; do
22
+ # Check if the server process is still running
23
+ if ! kill -0 "$PID" 2>/dev/null; then
24
+ echo "Server process ended. Exiting."
18
25
  exit 0
26
+ fi
27
+ sleep 5
28
+ done
19
29
  else
20
30
  # PID exists but not listening on port - stale PID file
21
31
  echo "Removing stale PID file (pid: $PID not listening on port $PORT)"
@@ -32,8 +42,15 @@ fi
32
42
  if lsof -ti:$PORT -sTCP:LISTEN >/dev/null 2>&1; then
33
43
  EXISTING_PID=$(lsof -ti:$PORT -sTCP:LISTEN | head -1)
34
44
  echo "Port $PORT is already in use by process $EXISTING_PID"
35
- echo "Server may already be running. Exiting gracefully."
45
+ echo "Server may already be running. Keeping process alive for Foreman..."
46
+ # Keep the process alive by monitoring the existing server
47
+ while true; do
48
+ if ! lsof -ti:$PORT -sTCP:LISTEN >/dev/null 2>&1; then
49
+ echo "Port $PORT is now free. Exiting so Foreman can restart."
36
50
  exit 0
51
+ fi
52
+ sleep 5
53
+ done
37
54
  fi
38
55
 
39
56
  # Start the server normally