diff options
-rw-r--r-- | .olddoc.yml | 1 | ||||
-rw-r--r-- | ISSUES | 18 | ||||
-rw-r--r-- | Links | 2 | ||||
-rw-r--r-- | lib/unicorn.rb | 2 | ||||
-rw-r--r-- | lib/unicorn/configurator.rb | 13 | ||||
-rw-r--r-- | lib/unicorn/http_request.rb | 94 | ||||
-rw-r--r-- | lib/unicorn/http_server.rb | 1 | ||||
-rw-r--r-- | lib/unicorn/socket_helper.rb | 23 | ||||
-rw-r--r-- | lib/unicorn/stream_input.rb | 9 | ||||
-rw-r--r-- | lib/unicorn/tee_input.rb | 14 | ||||
-rw-r--r-- | t/test-lib.sh | 4 | ||||
-rw-r--r-- | test/exec/test_exec.rb | 9 | ||||
-rw-r--r-- | test/unit/test_ccc.rb | 90 | ||||
-rw-r--r-- | test/unit/test_http_parser.rb | 18 | ||||
-rw-r--r-- | test/unit/test_socket_helper.rb | 12 | ||||
-rw-r--r-- | test/unit/test_util.rb | 4 |
16 files changed, 248 insertions, 66 deletions
diff --git a/.olddoc.yml b/.olddoc.yml index ee2d306..cacc0ab 100644 --- a/.olddoc.yml +++ b/.olddoc.yml @@ -12,7 +12,6 @@ noindex: - TODO - unicorn_rails_1 public_email: unicorn-public@bogomips.org -private_email: unicorn@bogomips.org nntp_url: - nntp://news.public-inbox.org/inbox.comp.lang.ruby.unicorn - nntp://news.gmane.org/gmane.comp.lang.ruby.unicorn.general @@ -9,14 +9,16 @@ submit patches and/or obtain support after you have searched the * Cc: all participants in a thread or commit, as subscription is optional * Do not {top post}[http://catb.org/jargon/html/T/top-post.html] in replies * Quote as little as possible of the message you're replying to -* Do not send HTML mail or images, it will be flagged as spam -* Anonymous and pseudonymous messages will always be welcome. +* Do not send HTML mail or images, + they hurt reader privacy and will be flagged as spam +* Anonymous and pseudonymous messages will ALWAYS be welcome * The email submission port (587) is enabled on the bogomips.org MX: https://bogomips.org/unicorn-public/20141004232241.GA23908@dcvr.yhbt.net/t/ If your issue is of a sensitive nature or you're just shy in public, -then feel free to email us privately at mailto:unicorn@bogomips.org -instead and your issue will be handled discreetly. +use anonymity tools such as Tor or Mixmaster; and rely on the public +mail archives for responses. Be sure to scrub sensitive log messages +and such. If you don't get a response within a few days, we may have forgotten about it so feel free to ask again. @@ -64,14 +66,14 @@ document distributed with git) on guidelines for patch submission. == Contact Info * public: mailto:unicorn-public@bogomips.org -* private: mailto:unicorn@bogomips.org * nntp://news.gmane.org/gmane.comp.lang.ruby.unicorn.general * nntp://news.public-inbox.org/inbox.comp.lang.ruby.unicorn * https://bogomips.org/unicorn-public/ +* http://ou63pmih66umazou.onion/unicorn-public/ Mailing list subscription is optional, so Cc: all participants. -You can follow along via NNTP: +You can follow along via NNTP (read-only): nntp://news.public-inbox.org/inbox.comp.lang.ruby.unicorn nntp://news.gmane.org/gmane.comp.lang.ruby.unicorn.general @@ -79,6 +81,7 @@ You can follow along via NNTP: Or Atom feeds: https://bogomips.org/unicorn-public/new.atom + http://ou63pmih66umazou.onion/unicorn-public/new.atom The HTML archives at https://bogomips.org/unicorn-public/ also has links to per-thread Atom feeds and downloadable @@ -88,3 +91,6 @@ You may optionally subscribe via plain-text email: mailto:unicorn-public+subscribe@bogomips.org (and confirming the auto-reply) + +Just keep in mind we suck at delivering email, so using NNTP, +or Atom feeds might be a better bet... @@ -23,7 +23,7 @@ or services behind them. * {golden_brindle}[https://github.com/simonoff/golden_brindle] - tool to manage multiple unicorn instances/applications on a single server -* {raindrops}[http://raindrops.bogomips.org/] - real-time stats for +* {raindrops}[https://bogomips.org/raindrops/] - real-time stats for preforking Rack servers * {UnXF}[https://bogomips.org/unxf/] Un-X-Forward* the Rack environment, diff --git a/lib/unicorn.rb b/lib/unicorn.rb index f122563..4bd7bda 100644 --- a/lib/unicorn.rb +++ b/lib/unicorn.rb @@ -95,7 +95,7 @@ module Unicorn # returns an array of strings representing TCP listen socket addresses # and Unix domain socket paths. This is useful for use with - # Raindrops::Middleware under Linux: http://raindrops.bogomips.org/ + # Raindrops::Middleware under Linux: https://bogomips.org/raindrops/ def self.listener_names Unicorn::HttpServer::LISTENERS.map do |io| Unicorn::SocketHelper.sock_name(io) diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb index f69f220..f404aea 100644 --- a/lib/unicorn/configurator.rb +++ b/lib/unicorn/configurator.rb @@ -56,7 +56,7 @@ class Unicorn::Configurator :worker_exec => false, :preload_app => false, :check_client_connection => false, - :rewindable_input => true, # for Rack 2.x: (Rack::VERSION[0] <= 1), + :rewindable_input => true, :client_body_buffer_size => Unicorn::Const::MAX_BODY, } #:startdoc: @@ -515,13 +515,12 @@ class Unicorn::Configurator # Disabling rewindability can improve performance by lowering # I/O and memory usage for applications that accept uploads. # Keep in mind that the Rack 1.x spec requires - # \env[\"rack.input\"] to be rewindable, so this allows - # intentionally violating the current Rack 1.x spec. + # \env[\"rack.input\"] to be rewindable, + # but the Rack 2.x spec does not. # - # +rewindable_input+ defaults to +true+ when used with Rack 1.x for - # Rack conformance. When Rack 2.x is finalized, this will most - # likely default to +false+ while still conforming to the newer - # (less demanding) spec. + # +rewindable_input+ defaults to +true+ for compatibility. + # Setting it to +false+ may be safe for applications and + # frameworks developed for Rack 2.x and later. def rewindable_input(bool) set_bool(:rewindable_input, bool) end diff --git a/lib/unicorn/http_request.rb b/lib/unicorn/http_request.rb index c176083..7253497 100644 --- a/lib/unicorn/http_request.rb +++ b/lib/unicorn/http_request.rb @@ -2,6 +2,7 @@ # :enddoc: # no stable API here require 'unicorn_http' +require 'raindrops' # TODO: remove redundant names Unicorn.const_set(:HttpRequest, Unicorn::HttpParser) @@ -25,8 +26,10 @@ class Unicorn::HttpParser # :stopdoc: HTTP_RESPONSE_START = [ 'HTTP'.freeze, '/1.1 '.freeze ] + EMPTY_ARRAY = [].freeze @@input_class = Unicorn::TeeInput @@check_client_connection = false + @@tcpi_inspect_ok = true def self.input_class @@input_class @@ -80,11 +83,7 @@ class Unicorn::HttpParser false until add_parse(socket.kgio_read!(16384)) end - # detect if the socket is valid by writing a partial response: - if @@check_client_connection && headers? - self.response_start_sent = true - HTTP_RESPONSE_START.each { |c| socket.write(c) } - end + check_client_connection(socket) if @@check_client_connection e['rack.input'] = 0 == content_length ? NULL_IO : @@input_class.new(socket, self) @@ -105,4 +104,89 @@ class Unicorn::HttpParser def hijacked? env.include?('rack.hijack_io'.freeze) end + + if Raindrops.const_defined?(:TCP_Info) + TCPI = Raindrops::TCP_Info.allocate + + def check_client_connection(socket) # :nodoc: + if Unicorn::TCPClient === socket + # Raindrops::TCP_Info#get!, #state (reads struct tcp_info#tcpi_state) + raise Errno::EPIPE, "client closed connection".freeze, + EMPTY_ARRAY if closed_state?(TCPI.get!(socket).state) + else + write_http_header(socket) + end + end + + if Raindrops.const_defined?(:TCP) + # raindrops 0.18.0+ supports FreeBSD + Linux using the same names + # Evaluate these hash lookups at load time so we can + # generate an opt_case_dispatch instruction + eval <<-EOS + def closed_state?(state) # :nodoc: + case state + when #{Raindrops::TCP[:ESTABLISHED]} + false + when #{Raindrops::TCP.values_at( + :CLOSE_WAIT, :TIME_WAIT, :CLOSE, :LAST_ACK, :CLOSING).join(',')} + true + else + false + end + end + EOS + else + # raindrops before 0.18 only supported TCP_INFO under Linux + def closed_state?(state) # :nodoc: + case state + when 1 # ESTABLISHED + false + when 8, 6, 7, 9, 11 # CLOSE_WAIT, TIME_WAIT, CLOSE, LAST_ACK, CLOSING + true + else + false + end + end + end + else + + # Ruby 2.2+ can show struct tcp_info as a string Socket::Option#inspect. + # Not that efficient, but probably still better than doing unnecessary + # work after a client gives up. + def check_client_connection(socket) # :nodoc: + if Unicorn::TCPClient === socket && @@tcpi_inspect_ok + opt = socket.getsockopt(:IPPROTO_TCP, :TCP_INFO).inspect + if opt =~ /\bstate=(\S+)/ + @@tcpi_inspect_ok = true + raise Errno::EPIPE, "client closed connection".freeze, + EMPTY_ARRAY if closed_state_str?($1) + else + @@tcpi_inspect_ok = false + write_http_header(socket) + end + opt.clear + else + write_http_header(socket) + end + end + + def closed_state_str?(state) + case state + when 'ESTABLISHED' + false + # not a typo, ruby maps TCP_CLOSE (no 'D') to state=CLOSED (w/ 'D') + when 'CLOSE_WAIT', 'TIME_WAIT', 'CLOSED', 'LAST_ACK', 'CLOSING' + true + else + false + end + end + end + + def write_http_header(socket) # :nodoc: + if headers? + self.response_start_sent = true + HTTP_RESPONSE_START.each { |c| socket.write(c) } + end + end end diff --git a/lib/unicorn/http_server.rb b/lib/unicorn/http_server.rb index 40a154d..3827f2e 100644 --- a/lib/unicorn/http_server.rb +++ b/lib/unicorn/http_server.rb @@ -89,6 +89,7 @@ class Unicorn::HttpServer @self_pipe = [] @workers = {} # hash maps PIDs to Workers @sig_queue = [] # signal queue used for self-piping + @pid = nil # we try inheriting listeners first, so we bind them later. # we don't write the pid file until we've bound listeners in case diff --git a/lib/unicorn/socket_helper.rb b/lib/unicorn/socket_helper.rb index df8315e..f52dde2 100644 --- a/lib/unicorn/socket_helper.rb +++ b/lib/unicorn/socket_helper.rb @@ -3,6 +3,18 @@ require 'socket' module Unicorn + + # Instead of using a generic Kgio::Socket for everything, + # tag TCP sockets so we can use TCP_INFO under Linux without + # incurring extra syscalls for Unix domain sockets. + # TODO: remove these when we remove kgio + TCPClient = Class.new(Kgio::Socket) # :nodoc: + class TCPSrv < Kgio::TCPServer # :nodoc: + def kgio_tryaccept # :nodoc: + super(TCPClient) + end + end + module SocketHelper # internal interface @@ -63,12 +75,15 @@ module Unicorn elsif respond_to?(:accf_arg) name = opt[:accept_filter] name = DEFAULTS[:accept_filter] if name.nil? + sock.listen(opt[:backlog]) + got = (sock.getsockopt(:SOL_SOCKET, :SO_ACCEPTFILTER) rescue nil).to_s + arg = accf_arg(name) begin - sock.setsockopt(:SOL_SOCKET, :SO_ACCEPTFILTER, accf_arg(name)) + sock.setsockopt(:SOL_SOCKET, :SO_ACCEPTFILTER, arg) rescue => e logger.error("#{sock_name(sock)} " \ "failed to set accept_filter=#{name} (#{e.inspect})") - end + end if arg != got end end @@ -148,7 +163,7 @@ module Unicorn end sock.bind(Socket.pack_sockaddr_in(port, addr)) sock.autoclose = false - Kgio::TCPServer.for_fd(sock.fileno) + TCPSrv.for_fd(sock.fileno) end # returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6 @@ -185,7 +200,7 @@ module Unicorn def server_cast(sock) begin Socket.unpack_sockaddr_in(sock.getsockname) - Kgio::TCPServer.for_fd(sock.fileno) + TCPSrv.for_fd(sock.fileno) rescue ArgumentError Kgio::UNIXServer.for_fd(sock.fileno) end diff --git a/lib/unicorn/stream_input.rb b/lib/unicorn/stream_input.rb index de5aeea..41d28a0 100644 --- a/lib/unicorn/stream_input.rb +++ b/lib/unicorn/stream_input.rb @@ -1,16 +1,17 @@ # -*- encoding: binary -*- -# When processing uploads, Unicorn may expose a StreamInput object under -# "rack.input" of the (future) Rack (2.x) environment. +# When processing uploads, unicorn may expose a StreamInput object under +# "rack.input" of the Rack environment when +# Unicorn::Configurator#rewindable_input is set to +false+ class Unicorn::StreamInput # The I/O chunk size (in +bytes+) for I/O operations where # the size cannot be user-specified when a method is called. # The default is 16 kilobytes. - @@io_chunk_size = Unicorn::Const::CHUNK_SIZE + @@io_chunk_size = Unicorn::Const::CHUNK_SIZE # :nodoc: # Initializes a new StreamInput object. You normally do not have to call # this unless you are writing an HTTP server. - def initialize(socket, request) + def initialize(socket, request) # :nodoc: @chunked = request.content_length.nil? @socket = socket @parser = request diff --git a/lib/unicorn/tee_input.rb b/lib/unicorn/tee_input.rb index 6f66162..2ccc2d9 100644 --- a/lib/unicorn/tee_input.rb +++ b/lib/unicorn/tee_input.rb @@ -1,6 +1,6 @@ # -*- encoding: binary -*- -# acts like tee(1) on an input input to provide a input-like stream +# Acts like tee(1) on an input input to provide a input-like stream # while providing rewindable semantics through a File/StringIO backing # store. On the first pass, the input is only read on demand so your # Rack application can use input notification (upload progress and @@ -9,22 +9,22 @@ # strict interpretation of Rack::Lint::InputWrapper functionality and # will not support any deviations from it. # -# When processing uploads, Unicorn exposes a TeeInput object under -# "rack.input" of the Rack environment. +# When processing uploads, unicorn exposes a TeeInput object under +# "rack.input" of the Rack environment by default. class Unicorn::TeeInput < Unicorn::StreamInput # The maximum size (in +bytes+) to buffer in memory before # resorting to a temporary file. Default is 112 kilobytes. - @@client_body_buffer_size = Unicorn::Const::MAX_BODY + @@client_body_buffer_size = Unicorn::Const::MAX_BODY # :nodoc: # sets the maximum size of request bodies to buffer in memory, # amounts larger than this are buffered to the filesystem - def self.client_body_buffer_size=(bytes) + def self.client_body_buffer_size=(bytes) # :nodoc: @@client_body_buffer_size = bytes end # returns the maximum size of request bodies to buffer in memory, # amounts larger than this are buffered to the filesystem - def self.client_body_buffer_size + def self.client_body_buffer_size # :nodoc: @@client_body_buffer_size end @@ -37,7 +37,7 @@ class Unicorn::TeeInput < Unicorn::StreamInput # Initializes a new TeeInput object. You normally do not have to call # this unless you are writing an HTTP server. - def initialize(socket, request) + def initialize(socket, request) # :nodoc: @len = request.content_length super @tmp = @len && @len <= @@client_body_buffer_size ? diff --git a/t/test-lib.sh b/t/test-lib.sh index 28d6a88..7f97958 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -106,8 +106,8 @@ check_stderr () { # unicorn_setup unicorn_setup () { eval $(unused_listen) - port=$(expr $listen : '[^:]*:\([0-9]\+\)') - host=$(expr $listen : '\([^:]*\):[0-9]\+') + port=$(expr $listen : '[^:]*:\([0-9]*\)') + host=$(expr $listen : '\([^:][^:]*\):[0-9][0-9]*') rtmpfiles unicorn_config pid r_err r_out fifo tmp ok cat > $unicorn_config <<EOF diff --git a/test/exec/test_exec.rb b/test/exec/test_exec.rb index ca0b7bc..08f92ae 100644 --- a/test/exec/test_exec.rb +++ b/test/exec/test_exec.rb @@ -97,6 +97,9 @@ run lambda { |env| end def test_sd_listen_fds_emulation + # [ruby-core:69895] [Bug #11336] fixed by r51576 + return if RUBY_VERSION.to_f < 2.3 + File.open("config.ru", "wb") { |fp| fp.write(HI) } sock = TCPServer.new(@addr, @port) @@ -124,9 +127,7 @@ run lambda { |env| end ensure sock.close if sock - # disabled test on old Rubies: https://bugs.ruby-lang.org/issues/11336 - # [ruby-core:69895] [Bug #11336] fixed by r51576 - end if RUBY_VERSION.to_f >= 2.3 + end def test_inherit_listener_unspecified File.open("config.ru", "wb") { |fp| fp.write(HI) } @@ -142,7 +143,7 @@ run lambda { |env| res = hit(["http://#@addr:#@port/"]) assert_equal [ "HI\n" ], res assert_shutdown(pid) - assert_equal 1, sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).int, + assert sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).bool, 'unicorn should always set SO_KEEPALIVE on inherited sockets' ensure sock.close if sock diff --git a/test/unit/test_ccc.rb b/test/unit/test_ccc.rb new file mode 100644 index 0000000..0db0c38 --- /dev/null +++ b/test/unit/test_ccc.rb @@ -0,0 +1,90 @@ +require 'socket' +require 'unicorn' +require 'io/wait' +require 'tempfile' +require 'test/unit' + +class TestCccTCPI < Test::Unit::TestCase + def test_ccc_tcpi + start_pid = $$ + host = '127.0.0.1' + srv = TCPServer.new(host, 0) + port = srv.addr[1] + err = Tempfile.new('unicorn_ccc') + rd, wr = IO.pipe + sleep_pipe = IO.pipe + pid = fork do + sleep_pipe[1].close + reqs = 0 + rd.close + worker_pid = nil + app = lambda do |env| + worker_pid ||= begin + at_exit { wr.write(reqs.to_s) if worker_pid == $$ } + $$ + end + reqs += 1 + + # will wake up when writer closes + sleep_pipe[0].read if env['PATH_INFO'] == '/sleep' + + [ 200, [ %w(Content-Length 0), %w(Content-Type text/plain) ], [] ] + end + ENV['UNICORN_FD'] = srv.fileno.to_s + opts = { + listeners: [ "#{host}:#{port}" ], + stderr_path: err.path, + check_client_connection: true, + } + uni = Unicorn::HttpServer.new(app, opts) + uni.start.join + end + wr.close + + # make sure the server is running, at least + client = TCPSocket.new(host, port) + client.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + assert client.wait_readable(10), 'never got response from server' + res = client.read + assert_match %r{\AHTTP/1\.1 200}, res, 'got part of first response' + assert_match %r{\r\n\r\n\z}, res, 'got end of response, server is ready' + client.close + + # start a slow request... + sleeper = TCPSocket.new(host, port) + sleeper.write("GET /sleep HTTP/1.1\r\nHost: example.com\r\n\r\n") + + # and a bunch of aborted ones + nr = 100 + nr.times do |i| + client = TCPSocket.new(host, port) + client.write("GET /collections/#{rand(10000)} HTTP/1.1\r\n" \ + "Host: example.com\r\n\r\n") + client.close + end + sleep_pipe[1].close # wake up the reader in the worker + res = sleeper.read + assert_match %r{\AHTTP/1\.1 200}, res, 'got part of first sleeper response' + assert_match %r{\r\n\r\n\z}, res, 'got end of sleeper response' + sleeper.close + kpid = pid + pid = nil + Process.kill(:QUIT, kpid) + _, status = Process.waitpid2(kpid) + assert status.success? + reqs = rd.read.to_i + warn "server got #{reqs} requests with #{nr} CCC aborted\n" if $DEBUG + assert_operator reqs, :<, nr + assert_operator reqs, :>=, 2, 'first 2 requests got through, at least' + ensure + return if start_pid != $$ + srv.close if srv + if pid + Process.kill(:QUIT, pid) + _, status = Process.waitpid2(pid) + assert status.success? + end + err.close! if err + rd.close if rd + end +end diff --git a/test/unit/test_http_parser.rb b/test/unit/test_http_parser.rb index 7cbc0f8..31e6f71 100644 --- a/test/unit/test_http_parser.rb +++ b/test/unit/test_http_parser.rb @@ -851,24 +851,6 @@ class HttpParserTest < Test::Unit::TestCase assert_equal '', parser.env['HTTP_HOST'] end - # so we don't care about the portability of this test - # if it doesn't leak on Linux, it won't leak anywhere else - # unless your C compiler or platform is otherwise broken - LINUX_PROC_PID_STATUS = "/proc/self/status" - def test_memory_leak - match_rss = /^VmRSS:\s+(\d+)/ - if File.read(LINUX_PROC_PID_STATUS) =~ match_rss - before = $1.to_i - 1000000.times { Unicorn::HttpParser.new } - File.read(LINUX_PROC_PID_STATUS) =~ match_rss - after = $1.to_i - diff = after - before - assert(diff < 10000, "memory grew more than 10M: #{diff}") - end - end if RUBY_PLATFORM =~ /linux/ && - File.readable?(LINUX_PROC_PID_STATUS) && - !defined?(RUBY_ENGINE) - def test_memsize require 'objspace' if ObjectSpace.respond_to?(:memsize_of) diff --git a/test/unit/test_socket_helper.rb b/test/unit/test_socket_helper.rb index 7526e82..8699409 100644 --- a/test/unit/test_socket_helper.rb +++ b/test/unit/test_socket_helper.rb @@ -150,28 +150,31 @@ class TestSocketHelper < Test::Unit::TestCase end def test_tcp_defer_accept_default + return unless defined?(TCP_DEFER_ACCEPT) port = unused_port @test_addr name = "#@test_addr:#{port}" sock = bind_listen(name) cur = sock.getsockopt(Socket::SOL_TCP, TCP_DEFER_ACCEPT).unpack('i')[0] assert cur >= 1 - end if defined?(TCP_DEFER_ACCEPT) + end def test_tcp_defer_accept_disable + return unless defined?(TCP_DEFER_ACCEPT) port = unused_port @test_addr name = "#@test_addr:#{port}" sock = bind_listen(name, :tcp_defer_accept => false) cur = sock.getsockopt(Socket::SOL_TCP, TCP_DEFER_ACCEPT).unpack('i')[0] assert_equal 0, cur - end if defined?(TCP_DEFER_ACCEPT) + end def test_tcp_defer_accept_nr + return unless defined?(TCP_DEFER_ACCEPT) port = unused_port @test_addr name = "#@test_addr:#{port}" sock = bind_listen(name, :tcp_defer_accept => 60) cur = sock.getsockopt(Socket::SOL_TCP, TCP_DEFER_ACCEPT).unpack('i')[0] assert cur > 1 - end if defined?(TCP_DEFER_ACCEPT) + end def test_ipv6only port = begin @@ -186,6 +189,7 @@ class TestSocketHelper < Test::Unit::TestCase end def test_reuseport + return unless defined?(Socket::SO_REUSEPORT) port = unused_port @test_addr name = "#@test_addr:#{port}" sock = bind_listen(name, :reuseport => true) @@ -193,5 +197,5 @@ class TestSocketHelper < Test::Unit::TestCase assert_operator cur, :>, 0 rescue Errno::ENOPROTOOPT # kernel does not support SO_REUSEPORT (older Linux) - end if defined?(Socket::SO_REUSEPORT) + end end diff --git a/test/unit/test_util.rb b/test/unit/test_util.rb index 4d17a16..dc6302e 100644 --- a/test/unit/test_util.rb +++ b/test/unit/test_util.rb @@ -69,7 +69,7 @@ class TestUtil < Test::Unit::TestCase } } tmp.close! - end if STDIN.respond_to?(:external_encoding) + end def test_reopen_logs_renamed_with_internal_encoding tmp = Tempfile.new('') @@ -101,5 +101,5 @@ class TestUtil < Test::Unit::TestCase } } tmp.close! - end if STDIN.respond_to?(:external_encoding) + end end |