mega-fast-sys 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/mega-fast-sys.gemspec +12 -0
  3. data/thin-2.0.1/CHANGELOG +422 -0
  4. data/thin-2.0.1/README.md +88 -0
  5. data/thin-2.0.1/Rakefile +22 -0
  6. data/thin-2.0.1/bin/thin +6 -0
  7. data/thin-2.0.1/example/adapter.rb +32 -0
  8. data/thin-2.0.1/example/async_app.ru +126 -0
  9. data/thin-2.0.1/example/async_chat.ru +247 -0
  10. data/thin-2.0.1/example/async_tailer.ru +100 -0
  11. data/thin-2.0.1/example/config.ru +22 -0
  12. data/thin-2.0.1/example/monit_sockets +20 -0
  13. data/thin-2.0.1/example/monit_unixsock +20 -0
  14. data/thin-2.0.1/example/myapp.rb +1 -0
  15. data/thin-2.0.1/example/ramaze.ru +12 -0
  16. data/thin-2.0.1/example/thin.god +80 -0
  17. data/thin-2.0.1/example/thin_solaris_smf.erb +36 -0
  18. data/thin-2.0.1/example/thin_solaris_smf.readme.txt +150 -0
  19. data/thin-2.0.1/example/vlad.rake +72 -0
  20. data/thin-2.0.1/ext/thin_parser/common.rl +59 -0
  21. data/thin-2.0.1/ext/thin_parser/ext_help.h +14 -0
  22. data/thin-2.0.1/ext/thin_parser/extconf.rb +6 -0
  23. data/thin-2.0.1/ext/thin_parser/parser.c +1447 -0
  24. data/thin-2.0.1/ext/thin_parser/parser.h +49 -0
  25. data/thin-2.0.1/ext/thin_parser/parser.rl +152 -0
  26. data/thin-2.0.1/ext/thin_parser/thin.c +435 -0
  27. data/thin-2.0.1/lib/rack/adapter/loader.rb +229 -0
  28. data/thin-2.0.1/lib/rack/adapter/rails.rb +172 -0
  29. data/thin-2.0.1/lib/rack/handler/thin.rb +13 -0
  30. data/thin-2.0.1/lib/rackup/handler/thin.rb +13 -0
  31. data/thin-2.0.1/lib/thin/backends/base.rb +169 -0
  32. data/thin-2.0.1/lib/thin/backends/swiftiply_client.rb +66 -0
  33. data/thin-2.0.1/lib/thin/backends/tcp_server.rb +34 -0
  34. data/thin-2.0.1/lib/thin/backends/unix_server.rb +56 -0
  35. data/thin-2.0.1/lib/thin/command.rb +53 -0
  36. data/thin-2.0.1/lib/thin/connection.rb +219 -0
  37. data/thin-2.0.1/lib/thin/controllers/cluster.rb +178 -0
  38. data/thin-2.0.1/lib/thin/controllers/controller.rb +189 -0
  39. data/thin-2.0.1/lib/thin/controllers/service.rb +76 -0
  40. data/thin-2.0.1/lib/thin/controllers/service.sh.erb +39 -0
  41. data/thin-2.0.1/lib/thin/daemonizing.rb +199 -0
  42. data/thin-2.0.1/lib/thin/env.rb +35 -0
  43. data/thin-2.0.1/lib/thin/headers.rb +47 -0
  44. data/thin-2.0.1/lib/thin/logging.rb +174 -0
  45. data/thin-2.0.1/lib/thin/rackup/handler.rb +33 -0
  46. data/thin-2.0.1/lib/thin/request.rb +159 -0
  47. data/thin-2.0.1/lib/thin/response.rb +144 -0
  48. data/thin-2.0.1/lib/thin/runner.rb +238 -0
  49. data/thin-2.0.1/lib/thin/server.rb +290 -0
  50. data/thin-2.0.1/lib/thin/stats.html.erb +216 -0
  51. data/thin-2.0.1/lib/thin/stats.rb +52 -0
  52. data/thin-2.0.1/lib/thin/statuses.rb +48 -0
  53. data/thin-2.0.1/lib/thin/version.rb +19 -0
  54. data/thin-2.0.1/lib/thin.rb +45 -0
  55. metadata +94 -0
@@ -0,0 +1,189 @@
1
+ require 'yaml'
2
+
3
+ module Thin
4
+ # Error raised that will abort the process and print not backtrace.
5
+ class RunnerError < RuntimeError; end
6
+
7
+ # Raised when a mandatory option is missing to run a command.
8
+ class OptionRequired < RunnerError
9
+ def initialize(option)
10
+ super("#{option} option required")
11
+ end
12
+ end
13
+
14
+ # Raised when an option is not valid.
15
+ class InvalidOption < RunnerError; end
16
+
17
+ # Build and control Thin servers.
18
+ # Hey Controller pattern is not only for web apps yo!
19
+ module Controllers
20
+ # Controls one Thin server.
21
+ # Allow to start, stop, restart and configure a single thin server.
22
+ class Controller
23
+ include Logging
24
+
25
+ # Command line options passed to the thin script
26
+ attr_accessor :options
27
+
28
+ def initialize(options)
29
+ @options = options
30
+
31
+ if @options[:socket]
32
+ @options.delete(:address)
33
+ @options.delete(:port)
34
+ end
35
+ end
36
+
37
+ def start
38
+ # Constantize backend class
39
+ @options[:backend] = eval(@options[:backend], TOPLEVEL_BINDING) if @options[:backend]
40
+
41
+ server = Server.new(@options[:socket] || @options[:address], # Server detects kind of socket
42
+ @options[:port], # Port ignored on UNIX socket
43
+ @options)
44
+
45
+ # Set options
46
+ server.pid_file = @options[:pid]
47
+ server.log_file = @options[:log]
48
+ server.timeout = @options[:timeout]
49
+ server.maximum_connections = @options[:max_conns]
50
+ server.maximum_persistent_connections = @options[:max_persistent_conns]
51
+ server.threaded = @options[:threaded]
52
+ server.no_epoll = @options[:no_epoll] if server.backend.respond_to?(:no_epoll=)
53
+ server.threadpool_size = @options[:threadpool_size] if server.threaded?
54
+
55
+ # ssl support
56
+ if @options[:ssl]
57
+ server.ssl = true
58
+ server.ssl_options = { :private_key_file => @options[:ssl_key_file], :cert_chain_file => @options[:ssl_cert_file], :verify_peer => !@options[:ssl_disable_verify], :ssl_version => @options[:ssl_version], :cipher_list => @options[:ssl_cipher_list]}
59
+ end
60
+
61
+ # Detach the process, after this line the current process returns
62
+ server.daemonize if @options[:daemonize]
63
+
64
+ # +config+ must be called before changing privileges since it might require superuser power.
65
+ server.config
66
+
67
+ server.change_privilege @options[:user], @options[:group] if @options[:user] && @options[:group]
68
+
69
+ # If a Rack config file is specified we eval it inside a Rack::Builder block to create
70
+ # a Rack adapter from it. Or else we guess which adapter to use and load it.
71
+ if @options[:rackup]
72
+ server.app = load_rackup_config
73
+ else
74
+ server.app = load_adapter
75
+ end
76
+
77
+ # If a prefix is required, wrap in Rack URL mapper
78
+ server.app = Rack::URLMap.new(@options[:prefix] => server.app) if @options[:prefix]
79
+
80
+ # If a stats URL is specified, wrap in Stats adapter
81
+ server.app = Stats::Adapter.new(server.app, @options[:stats]) if @options[:stats]
82
+
83
+ # Register restart procedure which just start another process with same options,
84
+ # so that's why this is done here.
85
+ server.on_restart { Command.run(:start, @options) }
86
+
87
+ server.start
88
+ end
89
+
90
+ def stop
91
+ raise OptionRequired, :pid unless @options[:pid]
92
+
93
+ tail_log(@options[:log]) do
94
+ if Server.kill(@options[:pid], @options[:force] ? 0 : (@options[:timeout] || 60))
95
+ wait_for_file :deletion, @options[:pid]
96
+ end
97
+ end
98
+ end
99
+
100
+ def restart
101
+ raise OptionRequired, :pid unless @options[:pid]
102
+
103
+ tail_log(@options[:log]) do
104
+ if Server.restart(@options[:pid])
105
+ wait_for_file :creation, @options[:pid]
106
+ end
107
+ end
108
+ end
109
+
110
+ def config
111
+ config_file = @options.delete(:config) || raise(OptionRequired, :config)
112
+
113
+ # Stringify keys
114
+ @options.keys.each { |o| @options[o.to_s] = @options.delete(o) }
115
+
116
+ File.open(config_file, 'w') { |f| f << @options.to_yaml }
117
+ log_info "Wrote configuration to #{config_file}"
118
+ end
119
+
120
+ protected
121
+ # Wait for a pid file to either be created or deleted.
122
+ def wait_for_file(state, file)
123
+ Timeout.timeout(@options[:timeout] || 30) do
124
+ case state
125
+ when :creation then sleep 0.1 until File.exist?(file)
126
+ when :deletion then sleep 0.1 while File.exist?(file)
127
+ end
128
+ end
129
+ end
130
+
131
+ # Tail the log file of server +number+ during the execution of the block.
132
+ def tail_log(log_file)
133
+ if log_file
134
+ tail_thread = tail(log_file)
135
+ yield
136
+ tail_thread.kill
137
+ else
138
+ yield
139
+ end
140
+ end
141
+
142
+ # Acts like GNU tail command. Taken from Rails.
143
+ def tail(file)
144
+ cursor = File.exist?(file) ? File.size(file) : 0
145
+ last_checked = Time.now
146
+ tail_thread = Thread.new do
147
+ Thread.pass until File.exist?(file)
148
+ File.open(file, 'r') do |f|
149
+ loop do
150
+ f.seek cursor
151
+ if f.mtime > last_checked
152
+ last_checked = f.mtime
153
+ contents = f.read
154
+ cursor += contents.length
155
+ print contents
156
+ STDOUT.flush
157
+ end
158
+ sleep 0.1
159
+ end
160
+ end
161
+ end
162
+ sleep 1 if File.exist?(file) # HACK Give the thread a little time to open the file
163
+ tail_thread
164
+ end
165
+
166
+ private
167
+ def load_adapter
168
+ adapter = @options[:adapter] || Rack::Adapter.guess(@options[:chdir])
169
+ log_info "Using #{adapter} adapter"
170
+ Rack::Adapter.for(adapter, @options)
171
+ rescue Rack::AdapterNotFound => e
172
+ raise InvalidOption, e.message
173
+ end
174
+
175
+ def load_rackup_config
176
+ ENV['RACK_ENV'] = @options[:environment]
177
+ case @options[:rackup]
178
+ when /\.rb$/
179
+ Kernel.load(@options[:rackup])
180
+ Object.const_get(File.basename(@options[:rackup], '.rb').capitalize.to_sym)
181
+ when /\.ru$/
182
+ Rack::Adapter.load(@options[:rackup])
183
+ else
184
+ raise "Invalid rackup file. please specify either a .ru or .rb file"
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,76 @@
1
+ require 'erb'
2
+
3
+ module Thin
4
+ module Controllers
5
+ # System service controller to launch all servers which
6
+ # config files are in a directory.
7
+ class Service < Controller
8
+ INITD_PATH = File.directory?('/etc/rc.d') ? '/etc/rc.d/thin' : '/etc/init.d/thin'
9
+ DEFAULT_CONFIG_PATH = '/etc/thin'
10
+ TEMPLATE = File.dirname(__FILE__) + '/service.sh.erb'
11
+
12
+ def initialize(options)
13
+ super
14
+
15
+ raise PlatformNotSupported, 'Running as a service only supported on Linux' unless Thin.linux?
16
+ end
17
+
18
+ def config_path
19
+ @options[:all] || DEFAULT_CONFIG_PATH
20
+ end
21
+
22
+ def start
23
+ run :start
24
+ end
25
+
26
+ def stop
27
+ run :stop
28
+ end
29
+
30
+ def restart
31
+ run :restart
32
+ end
33
+
34
+ def install(config_files_path=DEFAULT_CONFIG_PATH)
35
+ if File.exist?(INITD_PATH)
36
+ log_info "Thin service already installed at #{INITD_PATH}"
37
+ else
38
+ log_info "Installing thin service at #{INITD_PATH} ..."
39
+ sh "mkdir -p #{File.dirname(INITD_PATH)}"
40
+ log_info "writing #{INITD_PATH}"
41
+ File.open(INITD_PATH, 'w') do |f|
42
+ f << ERB.new(File.read(TEMPLATE)).result(binding)
43
+ end
44
+ sh "chmod +x #{INITD_PATH}" # Make executable
45
+ end
46
+
47
+ sh "mkdir -p #{config_files_path}"
48
+
49
+ log_info ''
50
+ log_info "To configure thin to start at system boot:"
51
+ log_info "on RedHat like systems:"
52
+ log_info " sudo /sbin/chkconfig --level 345 #{NAME} on"
53
+ log_info "on Debian-like systems (Ubuntu):"
54
+ log_info " sudo /usr/sbin/update-rc.d -f #{NAME} defaults"
55
+ log_info "on Gentoo:"
56
+ log_info " sudo rc-update add #{NAME} default"
57
+ log_info ''
58
+ log_info "Then put your config files in #{config_files_path}"
59
+ end
60
+
61
+ private
62
+ def run(command)
63
+ Dir[config_path + '/*'].each do |config|
64
+ next if config.end_with?("~")
65
+ log_info "[#{command}] #{config} ..."
66
+ Command.run(command, :config => config, :daemonize => true)
67
+ end
68
+ end
69
+
70
+ def sh(cmd)
71
+ log_info cmd
72
+ system(cmd)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,39 @@
1
+ #!/bin/sh
2
+ ### BEGIN INIT INFO
3
+ # Provides: thin
4
+ # Required-Start: $local_fs $remote_fs
5
+ # Required-Stop: $local_fs $remote_fs
6
+ # Default-Start: 2 3 4 5
7
+ # Default-Stop: S 0 1 6
8
+ # Short-Description: thin initscript
9
+ # Description: thin
10
+ ### END INIT INFO
11
+
12
+ # Original author: Forrest Robertson
13
+
14
+ # Do NOT "set -e"
15
+
16
+ DAEMON=<%= Command.script %>
17
+ SCRIPT_NAME=<%= INITD_PATH %>
18
+ CONFIG_PATH=<%= config_files_path %>
19
+
20
+ # Exit if the package is not installed
21
+ [ -x "$DAEMON" ] || exit 0
22
+
23
+ case "$1" in
24
+ start)
25
+ $DAEMON start --all $CONFIG_PATH
26
+ ;;
27
+ stop)
28
+ $DAEMON stop --all $CONFIG_PATH
29
+ ;;
30
+ restart)
31
+ $DAEMON restart --all $CONFIG_PATH
32
+ ;;
33
+ *)
34
+ echo "Usage: $SCRIPT_NAME {start|stop|restart}" >&2
35
+ exit 3
36
+ ;;
37
+ esac
38
+
39
+ :
@@ -0,0 +1,199 @@
1
+ require 'etc'
2
+ require 'daemons' unless Thin.win?
3
+
4
+ module Process
5
+ # Returns +true+ the process identied by +pid+ is running.
6
+ def running?(pid)
7
+ Process.getpgid(pid) != -1
8
+ rescue Errno::EPERM
9
+ true
10
+ rescue Errno::ESRCH
11
+ false
12
+ end
13
+ module_function :running?
14
+ end
15
+
16
+ module Thin
17
+ # Raised when the pid file already exist starting as a daemon.
18
+ class PidFileExist < RuntimeError; end
19
+ class PidFileNotFound < RuntimeError; end
20
+
21
+ # Module included in classes that can be turned into a daemon.
22
+ # Handle stuff like:
23
+ # * storing the PID in a file
24
+ # * redirecting output to the log file
25
+ # * changing process privileges
26
+ # * killing the process gracefully
27
+ module Daemonizable
28
+ attr_accessor :pid_file, :log_file
29
+
30
+ def self.included(base)
31
+ base.extend ClassMethods
32
+ end
33
+
34
+ def pid
35
+ File.exist?(pid_file) && !File.zero?(pid_file) ? open(pid_file).read.to_i : nil
36
+ end
37
+
38
+ def kill(timeout = 60)
39
+ if File.exist?(@pid_file)
40
+ self.class.kill(@pid_file, timeout)
41
+ end
42
+ end
43
+
44
+ # Turns the current script into a daemon process that detaches from the console.
45
+ def daemonize
46
+ raise PlatformNotSupported, 'Daemonizing is not supported on Windows' if Thin.win?
47
+ raise ArgumentError, 'You must specify a pid_file to daemonize' unless @pid_file
48
+
49
+ remove_stale_pid_file
50
+
51
+ pwd = Dir.pwd # Current directory is changed during daemonization, so store it
52
+
53
+ # HACK we need to create the directory before daemonization to prevent a bug under 1.9
54
+ # ignoring all signals when the directory is created after daemonization.
55
+ FileUtils.mkdir_p File.dirname(@pid_file)
56
+ FileUtils.mkdir_p File.dirname(@log_file)
57
+
58
+ Daemonize.daemonize(File.expand_path(@log_file), name)
59
+
60
+ Dir.chdir(pwd)
61
+
62
+ write_pid_file
63
+
64
+ at_exit do
65
+ log_info "Exiting!"
66
+ remove_pid_file
67
+ end
68
+ end
69
+
70
+ # Change privileges of the process
71
+ # to the specified user and group.
72
+ def change_privilege(user, group=user)
73
+ log_info "Changing process privilege to #{user}:#{group}"
74
+
75
+ uid, gid = Process.euid, Process.egid
76
+ target_uid = Etc.getpwnam(user).uid
77
+ target_gid = Etc.getgrnam(group).gid
78
+
79
+ if uid != target_uid || gid != target_gid
80
+ # Change PID file ownership
81
+ File.chown(target_uid, target_gid, @pid_file) if File.exist?(@pid_file)
82
+
83
+ # Change process ownership
84
+ Process.initgroups(user, target_gid)
85
+ Process::GID.change_privilege(target_gid)
86
+ Process::UID.change_privilege(target_uid)
87
+
88
+ # Correct environment variables
89
+ ENV.store('USER', user)
90
+ ENV.store('HOME', File.expand_path("~#{user}"))
91
+ end
92
+ rescue Errno::EPERM => e
93
+ log_info "Couldn't change user and group to #{user}:#{group}: #{e}"
94
+ end
95
+
96
+ # Register a proc to be called to restart the server.
97
+ def on_restart(&block)
98
+ @on_restart = block
99
+ end
100
+
101
+ # Restart the server.
102
+ def restart
103
+ if @on_restart
104
+ log_info 'Restarting ...'
105
+ stop
106
+ remove_pid_file
107
+ @on_restart.call
108
+ EM.next_tick { exit! }
109
+ end
110
+ end
111
+
112
+ module ClassMethods
113
+ # Send a QUIT or INT (if timeout is +0+) signal the process which
114
+ # PID is stored in +pid_file+.
115
+ # If the process is still running after +timeout+, KILL signal is
116
+ # sent.
117
+ def kill(pid_file, timeout=60)
118
+ if timeout == 0
119
+ send_signal('INT', pid_file, timeout)
120
+ else
121
+ send_signal('QUIT', pid_file, timeout)
122
+ end
123
+ end
124
+
125
+ # Restart the server by sending HUP signal.
126
+ def restart(pid_file)
127
+ send_signal('HUP', pid_file)
128
+ end
129
+
130
+ def monotonic_time
131
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
132
+ end
133
+
134
+ # Send a +signal+ to the process which PID is stored in +pid_file+.
135
+ def send_signal(signal, pid_file, timeout=60)
136
+ if pid = read_pid_file(pid_file)
137
+ Logging.log_info "Sending #{signal} signal to process #{pid} ... "
138
+
139
+ Process.kill(signal, pid)
140
+
141
+ # This loop seems kind of racy to me...
142
+ started_at = monotonic_time
143
+ while Process.running?(pid)
144
+ sleep 0.1
145
+ raise Timeout::Error if (monotonic_time - started_at) > timeout
146
+ end
147
+ else
148
+ raise PidFileNotFound, "Can't stop process, no PID found in #{pid_file}"
149
+ end
150
+ rescue Timeout::Error
151
+ Logging.log_info "Timeout!"
152
+ force_kill(pid, pid_file)
153
+ rescue Interrupt
154
+ force_kill(pid, pid_file)
155
+ rescue Errno::ESRCH # No such process
156
+ Logging.log_info "process not found!"
157
+ force_kill(pid, pid_file)
158
+ end
159
+
160
+ def force_kill(pid, pid_file)
161
+ Logging.log_info "Sending KILL signal to process #{pid} ... "
162
+ Process.kill("KILL", pid)
163
+ File.delete(pid_file) if File.exist?(pid_file)
164
+ end
165
+
166
+ def read_pid_file(file)
167
+ if File.file?(file) && pid = File.read(file)
168
+ pid.to_i
169
+ else
170
+ nil
171
+ end
172
+ end
173
+ end
174
+
175
+ protected
176
+ def remove_pid_file
177
+ File.delete(@pid_file) if @pid_file && File.exist?(@pid_file)
178
+ end
179
+
180
+ def write_pid_file
181
+ log_info "Writing PID to #{@pid_file}"
182
+ open(@pid_file,"w") { |f| f.write(Process.pid) }
183
+ File.chmod(0644, @pid_file)
184
+ end
185
+
186
+ # If PID file is stale, remove it.
187
+ def remove_stale_pid_file
188
+ if File.exist?(@pid_file)
189
+ if pid && Process.running?(pid)
190
+ raise PidFileExist, "#{@pid_file} already exists, seems like it's already running (process ID: #{pid}). " +
191
+ "Stop the process or delete #{@pid_file}."
192
+ else
193
+ log_info "Deleting stale PID file #{@pid_file}"
194
+ remove_pid_file
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,35 @@
1
+
2
+ module Thin
3
+ module Env
4
+ def self.with_defaults(env)
5
+ if ::Rack.release >= "3"
6
+ rack_env_class = Rack3
7
+ else
8
+ rack_env_class = Rack2
9
+ end
10
+
11
+ rack_env_class.env.merge(env)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ class Rack2
18
+ RACK_VERSION = [1, 0].freeze
19
+
20
+ def self.env
21
+ {
22
+ ::Thin::Request::RACK_VERSION => RACK_VERSION,
23
+ ::Thin::Request::RACK_MULTITHREAD => false,
24
+ ::Thin::Request::RACK_MULTIPROCESS => false,
25
+ ::Thin::Request::RACK_RUN_ONCE => false
26
+ }
27
+ end
28
+ end
29
+
30
+ class Rack3
31
+ def self.env
32
+ {}
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ module Thin
2
+ # Raised when an header is not valid
3
+ # and the server can not process it.
4
+ class InvalidHeader < StandardError; end
5
+
6
+ # Store HTTP header name-value pairs direcly to a string
7
+ # and allow duplicated entries on some names.
8
+ class Headers
9
+ HEADER_FORMAT = "%s: %s\r\n".freeze
10
+ ALLOWED_DUPLICATES = %w(set-cookie set-cookie2 warning www-authenticate).freeze
11
+ CR_OR_LF = /[\r\n]/.freeze
12
+
13
+ def initialize
14
+ @sent = {}
15
+ @out = []
16
+ end
17
+
18
+ # Add <tt>key: value</tt> pair to the headers.
19
+ # Ignore if already sent and no duplicates are allowed
20
+ # for this +key+.
21
+ def []=(key, value)
22
+ downcase_key = key.downcase
23
+ if !@sent.has_key?(downcase_key) || ALLOWED_DUPLICATES.include?(downcase_key)
24
+ @sent[downcase_key] = true
25
+ value = case value
26
+ when Time
27
+ value.httpdate
28
+ when NilClass
29
+ return
30
+ when CR_OR_LF
31
+ raise InvalidHeader, "Header contains CR or LF"
32
+ else
33
+ value.to_s
34
+ end
35
+ @out << HEADER_FORMAT % [key, value]
36
+ end
37
+ end
38
+
39
+ def has_key?(key)
40
+ @sent[key.downcase]
41
+ end
42
+
43
+ def to_s
44
+ @out.join
45
+ end
46
+ end
47
+ end