require "open3"
require "timeout"

module CC
  module Engine
    module Analyzers
      class CommandLineRunner
        DEFAULT_TIMEOUT = 300

        def initialize(command, timeout = DEFAULT_TIMEOUT)
          @command = command
          @timeout = timeout
        end

        def run(input)
          Timeout.timeout(timeout) do
            Open3.popen3 command, "r+" do |stdin, stdout, stderr, wait_thr|
              stdin.puts input
              stdin.close

              exit_code = wait_thr.value

              output = stdout.gets
              stdout.close

              err_output = stderr.gets
              stderr.close

              if 0 == exit_code
                yield output
              else
                raise ::CC::Engine::Analyzers::ParserError, "Python parser exited with code #{exit_code}:\n#{err_output}"
              end
            end
          end
        end

        private

        attr_reader :command, :timeout
      end
    end
  end
end


require "open3"
require "timeout"

module CC
  module Engine
    module Analyzers
      class CommandLineRunner
        DEFAULT_TIMEOUT = 300

        def initialize(command, timeout = DEFAULT_TIMEOUT)
          @command = command
          @timeout = timeout
        end

        def run(input)
          Timeout.timeout(timeout) do
            out, err, status = Open3.capture3(command, stdin_data: input)
            if status.success?
              yield out
            else
              raise ::CC::Engine::Analyzers::ParserError, "`#{command}` exited with code #{status.exitstatus}:\n#{err}"
            end
          end
        end

        private

        attr_reader :command, :timeout
      end
    end
  end
end


Use .capture3 not .popen3 for external parsers
Use .capture3 not .popen3 for external parsers

In my testing, I believe this will fix the frequent timeouts we've seen
happen on specific files.

Like other issues we'd been seeing, these were actually
non-deterministic, they just happened to *almost* always fail. But I did
see them succeed occasionally: this may be partially why I didn't catch
this in testing before the prior release, since I ran it against some of
the same projects we saw failing. I just got lucky the first time
around.

What I saw locally was that sometimes the status thread given by `.open3` would
be dead before it was asked for its value, and in those cases the call
seemed to succeed. If the thread was still alive when asked for its
value, it seemed to then hang indefinitely.

Looking at the code for [`.capture3`][1], the biggest difference seems
to be the use of threads for capturing stdout/stderr. There's even a
warning in [stdlib docs about deadlocking from not doing this][2]! So I
think the previous implementation was effectively equivalent to a poor,
buggy implementation of `.capture3`, and we should just use the real one. It's
unclear to me if the older usage of `IO.popen` also suffered from this
behavior and we just happened to be swallowing it, or if the pipe buffer
behavior is a little different there.

[1]: https://github.com/jruby/jruby/blob/3dbb634f2764b24bca9e92ecd3d52d859cc0e847/lib/ruby/stdlib/open3.rb#L258-L274
[2]: http://ruby-doc.org/stdlib-2.2.3/libdoc/open3/rdoc/Open3.html#method-c-popen3
