lib/yard/server/commands/library_command.rb



# frozen_string_literal: true
require 'thread'

module YARD
  module Server
    module Commands
      class LibraryOptions < CLI::YardocOptions
        def adapter; @command.adapter end
        def library; @command.library end
        def single_library; @command.single_library end
        def serializer; @command.serializer end
        def serialize; false end

        attr_accessor :command
        attr_accessor :frames

        def each(&block)
          super(&block)
          yield(:adapter, adapter)
          yield(:library, library)
          yield(:single_library, single_library)
          yield(:serializer, serializer)
        end
      end

      # This is the base command for all commands that deal directly with libraries.
      # Some commands do not, but most (like {DisplayObjectCommand}) do. If your
      # command deals with libraries directly, subclass this class instead.
      # See {Base} for notes on how to subclass a command.
      #
      # @abstract
      class LibraryCommand < Base
        begin
          Process.fork { }
          CAN_FORK = true
        rescue Exception # rubocop:disable Lint/RescueException
          CAN_FORK = false
        end

        # @return [LibraryVersion] the object containing library information
        attr_accessor :library

        # @return [LibraryOptions] default options for the library
        attr_accessor :options

        # @return [Serializers::Base] the serializer used to perform file linking
        attr_accessor :serializer

        # @return [Boolean] whether router should route for multiple libraries
        attr_accessor :single_library

        # @return [Boolean] whether to reparse data
        attr_accessor :incremental

        # @return [Boolean] whether or not this adapter calls +fork+ when serving
        #   library requests. Defaults to false.
        attr_accessor :use_fork

        # Needed to synchronize threads in {#setup_yardopts}
        # @private
        @@library_chdir_lock = Mutex.new

        def initialize(opts = {})
          super
          self.serializer = DocServerSerializer.new
        end

        def call(request)
          if can_fork?
            call_with_fork(request) { super }
          else
            begin
              save_default_template_info
              call_without_fork(request) { super }
            ensure
              restore_template_info
            end
          end
        end

        private

        def call_without_fork(request)
          self.request = request
          self.options = LibraryOptions.new
          options.reset_defaults
          options.command = self
          setup_library
          options.title = "Documentation for #{library.name} " +
                          (library.version ? '(' + library.version + ')' : '')
          yield
        rescue LibraryNotPreparedError
          not_prepared
        end

        def call_with_fork(request, &block)
          IO.pipe(:binmode => true) do |reader, writer|
            fork do
              log.debug "[pid=#{Process.pid}] fork serving: #{request.path}"
              reader.close
              writer.print(Marshal.dump(call_without_fork(request, &block)))
            end

            writer.close
            Marshal.load(reader.read)
          end
        end

        def can_fork?
          CAN_FORK && use_fork
        end

        def save_default_template_info
          @old_template_paths = Templates::Engine.template_paths.dup
          @old_extra_includes = Templates::Template.extra_includes.dup
        end

        def restore_template_info
          Templates::Engine.template_paths = @old_template_paths
          Templates::Template.extra_includes = @old_extra_includes
        end

        def setup_library
          library.prepare! if request.xhr? && request.query['process']
          load_yardoc
          setup_yardopts
          true
        end

        def setup_yardopts
          @@library_chdir_lock.synchronize do
            Dir.chdir(library.source_path) do
              yardoc = CLI::Yardoc.new
              if incremental
                yardoc.run('-c', '-n', '--no-stats')
              else
                yardoc.parse_arguments
              end
              yardoc.send(:verify_markup_options)
              yardoc.options.delete(:serializer)
              yardoc.options.delete(:serialize)
              options.update(yardoc.options.to_hash)
            end
          end
        end

        def load_yardoc
          raise LibraryNotPreparedError unless library.ready?
          if Thread.current[:__yard_last_yardoc__] == library.yardoc_file
            log.debug "Reusing yardoc file: #{library.yardoc_file}"
            return
          end
          Registry.clear
          Templates::ErbCache.clear!
          Registry.load_yardoc(library.yardoc_file)
          Thread.current[:__yard_last_yardoc__] = library.yardoc_file
        end

        def not_prepared
          options.update(:template => :doc_server, :type => :processing)
          self.caching = false
          self.status = 202
          self.body = render
          self.headers = {'Content-Type' => 'text/html'}
          [status, headers, [body]]
        end

        # Hack to load a custom fulldoc template object that does
        # not do any rendering/generation. We need this to access the
        # generate_*_list methods.
        def fulldoc_template
          tplopts = [options.template, :fulldoc, options.format]
          tplclass = Templates::Engine.template(*tplopts)
          obj = Object.new.extend(tplclass)
          class << obj; define_method(:init) {} end
          obj.class = tplclass
          obj.send(:initialize, options)
          class << obj
            attr_reader :contents
            define_method(:asset) {|_, contents| @contents = contents }
          end
          obj
        end
      end
    end
  end
end