# frozen_string_literal: true module ThemeCheck module LanguageServer class IOMessenger < Messenger def self.err_stream if ThemeCheck.debug_log_file File.open(ThemeCheck.debug_log_file, "w") else STDERR end end def initialize( in_stream: STDIN, out_stream: STDOUT, err_stream: IOMessenger.err_stream ) validate!([in_stream, out_stream, err_stream]) @in = in_stream @out = out_stream @err = err_stream # Because programming is fun, # # Ruby on Windows turns \n into \r\n. Which means that \r\n # gets turned into \r\r\n. Which means that the protocol # breaks on windows unless we turn STDOUT into binary mode. # # Hours wasted: 9. @out.binmode @out.sync = true # do not buffer @err.sync = true # do not buffer # Lock for writing, otherwise messages might be interspersed. @writer = Mutex.new end def read_message length = initial_line.match(/Content-Length: (\d+)/)[1].to_i content = '' length_to_read = 2 + length # 2 is the empty line length (\r\n) while content.length < length_to_read chunk = @in.read(length_to_read - content.length) raise DoneStreaming if chunk.nil? content += chunk end content.lstrip! rescue IOError raise DoneStreaming end def send_message(message_body) @writer.synchronize do @out.write("Content-Length: #{message_body.bytesize}\r\n") @out.write("\r\n") @out.write(message_body) @out.flush end end def log(message) @err.puts(message) @err.flush end def close_input @in.close unless @in.closed? end def close_output @err.close @out.close end private def initial_line # Scanning for lines that fit the protocol. while true initial_line = @in.gets # gets returning nil means the stream was closed. raise DoneStreaming if initial_line.nil? if initial_line.match(/Content-Length: (\d+)/) break end end initial_line end def supported_io_classes [IO, StringIO] end def validate!(streams = []) streams.each do |stream| unless supported_io_classes.find { |klass| stream.is_a?(klass) } raise IncompatibleStream, incompatible_stream_message end end end def incompatible_stream_message 'if provided, in_stream, out_stream, and err_stream must be a kind of '\ "one of the following: #{supported_io_classes.join(', ')}" end end end end