@@ -38,6 +38,14 @@ class RDoc::Server
3838 '.json' => 'application/json' ,
3939 } . freeze
4040
41+ STATUS_TEXTS = {
42+ 200 => 'OK' ,
43+ 400 => 'Bad Request' ,
44+ 404 => 'Not Found' ,
45+ 405 => 'Method Not Allowed' ,
46+ 500 => 'Internal Server Error' ,
47+ } . freeze
48+
4149 ##
4250 # Creates a new server.
4351 #
@@ -56,15 +64,17 @@ def initialize(rdoc, port)
5664 @search_index_cache = nil
5765 @last_change_time = Time . now . to_f
5866 @mutex = Mutex . new
67+ @running = false
5968 end
6069
6170 ##
6271 # Starts the server. Blocks until interrupted.
6372
6473 def start
65- @tcp_server = TCPServer . new ( '0.0.0.0' , @port )
74+ @tcp_server = TCPServer . new ( '127.0.0.1' , @port )
75+ @running = true
6676
67- start_watcher ( @rdoc . last_modified . keys )
77+ @watcher_thread = start_watcher ( @rdoc . last_modified . keys )
6878
6979 url = "http://localhost:#{ @port } "
7080 $stderr. puts "\n Serving documentation at: \e ]8;;#{ url } \e \\ #{ url } \e ]8;;\e \\ "
@@ -82,7 +92,9 @@ def start
8292 # Shuts down the server.
8393
8494 def shutdown
95+ @running = false
8596 @tcp_server &.close
97+ @watcher_thread &.join ( 2 )
8698 end
8799
88100 private
@@ -101,18 +113,27 @@ def create_generator
101113 def handle_client ( client )
102114 client . binmode
103115
116+ return unless IO . select ( [ client ] , nil , nil , 5 )
117+
104118 request_line = client . gets ( "\n " )
105119 return unless request_line
106120
107121 method , request_uri , = request_line . split ( ' ' , 3 )
108- path = URI . parse ( request_uri ) . path
122+ return write_response ( client , 400 , 'text/plain' , 'Bad Request' ) unless request_uri
123+
124+ begin
125+ path = URI . parse ( request_uri ) . path
126+ rescue URI ::InvalidURIError
127+ return write_response ( client , 400 , 'text/plain' , 'Bad Request' )
128+ end
109129
110- # Consume remaining headers (we don't need them)
111130 while ( line = client . gets ( "\n " ) )
112131 break if line . strip . empty?
113132 end
114133
115- return unless method == 'GET'
134+ unless method == 'GET'
135+ return write_response ( client , 405 , 'text/plain' , 'Method Not Allowed' )
136+ end
116137
117138 status , content_type , body = route ( path )
118139 write_response ( client , status , content_type , body )
@@ -134,8 +155,10 @@ def handle_client(client)
134155 def route ( path )
135156 case path
136157 when '/__status'
137- [ 200 , 'application/json' , JSON . generate ( last_change : @last_change_time ) ]
158+ t = @mutex . synchronize { @last_change_time }
159+ [ 200 , 'application/json' , JSON . generate ( last_change : t ) ]
138160 when '/js/search_data.js'
161+ # Search data is dynamically generated, not a static asset
139162 serve_page ( path )
140163 when %r{\A /(?:css|js)/}
141164 serve_asset ( path )
@@ -148,10 +171,9 @@ def route(path)
148171 # Writes an HTTP/1.1 response to +client+.
149172
150173 def write_response ( client , status , content_type , body )
151- status_text = { 200 => 'OK' , 404 => 'Not Found' , 500 => 'Internal Server Error' }
152174 body_bytes = body . b
153175
154- header = +"HTTP/1.1 #{ status } #{ status_text [ status ] || 'Unknown' } \r \n "
176+ header = +"HTTP/1.1 #{ status } #{ STATUS_TEXTS [ status ] || 'Unknown' } \r \n "
155177 header << "Content-Type: #{ content_type } \r \n "
156178 header << "Content-Length: #{ body_bytes . bytesize } \r \n "
157179 header << "Connection: close\r \n "
@@ -168,14 +190,16 @@ def write_response(client, status, content_type, body)
168190 def serve_asset ( path )
169191 rel_path = path . sub ( %r{\A /} , '' )
170192 asset_path = File . join ( @generator . template_dir , rel_path )
193+ real_asset = File . expand_path ( asset_path )
194+ real_template = File . expand_path ( @generator . template_dir )
171195
172- unless File . file? ( asset_path )
196+ unless real_asset . start_with? ( " #{ real_template } /" ) && File . file? ( real_asset )
173197 return [ 404 , 'text/plain' , "Asset not found: #{ rel_path } " ]
174198 end
175199
176200 ext = File . extname ( rel_path )
177201 content_type = CONTENT_TYPES [ ext ] || 'application/octet-stream'
178- [ 200 , content_type , File . read ( asset_path ) ]
202+ [ 200 , content_type , File . read ( real_asset ) ]
179203 end
180204
181205 ##
@@ -242,10 +266,8 @@ def generate_page(name)
242266 # Builds the search index JavaScript.
243267
244268 def build_search_index
245- @search_index_cache ||= begin
246- index = @generator . build_search_index
247- "var search_data = #{ JSON . generate ( index : index ) } ;"
248- end
269+ @search_index_cache ||=
270+ "var search_data = #{ JSON . generate ( index : @generator . build_search_index ) } ;"
249271 end
250272
251273 ##
@@ -273,11 +295,13 @@ def start_watcher(source_files)
273295 end
274296
275297 Thread . new do
276- loop do
277- sleep 1
278- check_for_changes
279- rescue => e
280- $stderr. puts "RDoc server watcher error: #{ e . message } "
298+ while @running
299+ begin
300+ sleep 1
301+ check_for_changes
302+ rescue => e
303+ $stderr. puts "RDoc server watcher error: #{ e . message } "
304+ end
281305 end
282306 end
283307 end
@@ -290,7 +314,6 @@ def check_for_changes
290314 changed = [ ]
291315 removed = [ ]
292316
293- # Check existing tracked files for modifications or deletions
294317 @file_mtimes . each do |file , old_mtime |
295318 unless File . exist? ( file )
296319 removed << file
@@ -302,7 +325,6 @@ def check_for_changes
302325 changed << file if old_mtime . nil? || current_mtime > old_mtime
303326 end
304327
305- # Scan for new files using the same directory walking logic
306328 file_list = @rdoc . normalized_file_list (
307329 @options . files . empty? ? [ @options . root . to_s ] : @options . files ,
308330 true , @options . exclude
@@ -346,24 +368,32 @@ def relative_path_for(filename)
346368 # refreshes the generator, and invalidates caches.
347369
348370 def reparse_and_refresh ( changed_files , removed_files )
349- unless removed_files . empty?
350- $stderr. puts "Removed: #{ removed_files . join ( ', ' ) } "
351- removed_files . each do |f |
352- @file_mtimes . delete ( f )
353- relative = relative_path_for ( f )
354- @store . remove_file ( relative )
371+ @mutex . synchronize do
372+ unless removed_files . empty?
373+ $stderr. puts "Removed: #{ removed_files . join ( ', ' ) } "
374+ removed_files . each do |f |
375+ @file_mtimes . delete ( f )
376+ relative = relative_path_for ( f )
377+ @store . remove_file ( relative )
378+ end
355379 end
356- end
357380
358- unless changed_files . empty?
359- $stderr. puts "Re-parsing: #{ changed_files . join ( ', ' ) } "
360- changed_files . each { |f | @rdoc . parse_file ( f ) }
361- changed_files . each { |f | @file_mtimes [ f ] = File . mtime ( f ) rescue nil }
362- end
381+ unless changed_files . empty?
382+ $stderr. puts "Re-parsing: #{ changed_files . join ( ', ' ) } "
383+ changed_files . each do |f |
384+ begin
385+ relative = relative_path_for ( f )
386+ @store . remove_file ( relative )
387+ @rdoc . parse_file ( f )
388+ @file_mtimes [ f ] = File . mtime ( f ) rescue nil
389+ rescue => e
390+ $stderr. puts "Error parsing #{ f } : #{ e . message } "
391+ end
392+ end
393+ end
363394
364- @rdoc . store . complete ( @options . visibility )
395+ @ store. complete ( @options . visibility )
365396
366- @mutex . synchronize do
367397 @generator . refresh_store_data
368398 invalidate_all_caches
369399 @last_change_time = Time . now . to_f
0 commit comments