pipeline_toolkit 1.1.0 → 1.2.0
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.
- data/LICENSE.markdown +1 -1
- data/README.markdown +38 -19
- data/bin/msg_generator +3 -4
- data/bin/msg_push +3 -4
- data/bin/msg_sink +3 -7
- data/bin/msg_subscribe +3 -4
- data/lib/pipeline_toolkit.rb +5 -5
- data/lib/pipeline_toolkit/amqp/abstract.rb +81 -78
- data/lib/pipeline_toolkit/amqp/reader.rb +55 -52
- data/lib/pipeline_toolkit/amqp/writer.rb +42 -39
- data/lib/pipeline_toolkit/{commands/msg_generator/cli.rb → cli/msg_generator_cli.rb} +6 -5
- data/lib/pipeline_toolkit/{commands/msg_push/cli.rb → cli/msg_push_cli.rb} +16 -8
- data/lib/pipeline_toolkit/cli/msg_sink_cli.rb +28 -0
- data/lib/pipeline_toolkit/{commands/msg_subscribe/cli.rb → cli/msg_subscribe_cli.rb} +23 -14
- data/lib/pipeline_toolkit/cucumber.rb +5 -3
- data/lib/pipeline_toolkit/cucumber/amqp.rb +1 -1
- data/lib/pipeline_toolkit/cucumber/in_out_process.rb +50 -0
- data/lib/pipeline_toolkit/cucumber/machine.rb +44 -26
- data/lib/pipeline_toolkit/cucumber/machine_steps.rb +15 -9
- data/lib/pipeline_toolkit/cucumber/{msg_sub_machine.rb → msg_sub_process.rb} +5 -5
- data/lib/pipeline_toolkit/handlers/message_handler.rb +43 -39
- data/lib/pipeline_toolkit/message_coder.rb +38 -25
- data/lib/pipeline_toolkit/message_command.rb +167 -157
- data/lib/pipeline_toolkit/message_generator.rb +22 -18
- data/lib/pipeline_toolkit/message_pusher.rb +46 -47
- data/lib/pipeline_toolkit/message_sink.rb +9 -5
- data/lib/pipeline_toolkit/message_subscriber.rb +190 -183
- data/lib/pipeline_toolkit/monitoring/monitor_server.rb +64 -109
- data/lib/pipeline_toolkit/util/hash_ext.rb +8 -0
- data/lib/pipeline_toolkit/util/indifferent_hash.rb +10 -0
- data/lib/pipeline_toolkit/{socket_util.rb → util/socket_util.rb} +1 -1
- metadata +25 -115
- data/lib/pipeline_toolkit/commands/msg_sink/cli.rb +0 -41
- data/lib/pipeline_toolkit/open_hash.rb +0 -24
@@ -1,27 +1,31 @@
|
|
1
1
|
require 'json'
|
2
2
|
|
3
|
-
|
3
|
+
module PipelineToolkit
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
class MessageGenerator
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@delay = options.delay
|
9
|
+
@msg = JSON.parse(options.msg)
|
10
|
+
@limit = options.limit
|
11
|
+
end
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
def start
|
14
|
+
@count = 0
|
15
|
+
loop do
|
16
|
+
@count += 1
|
17
|
+
break if @limit && @count > @limit
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
+
self.send_msg(@msg)
|
20
|
+
sleep(@delay) unless @delay.nil?
|
21
|
+
end
|
19
22
|
end
|
20
|
-
end
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
def send_msg(msg)
|
25
|
+
puts MessageCoder.encode(@msg)
|
26
|
+
$stdout.flush
|
27
|
+
end
|
26
28
|
|
29
|
+
end
|
30
|
+
|
27
31
|
end
|
@@ -1,58 +1,57 @@
|
|
1
1
|
require "mq"
|
2
2
|
|
3
|
-
|
4
|
-
# A Message Queue machine to handle messages that has to be published
|
5
|
-
# back into a message queue.
|
6
|
-
#
|
7
|
-
class MessagePusher
|
8
|
-
include MessageCommand
|
9
|
-
include Amqp::Writer
|
3
|
+
module PipelineToolkit
|
10
4
|
|
11
5
|
##
|
12
|
-
#
|
6
|
+
# A Message Queue machine to handle messages that has to be published
|
7
|
+
# back into a message queue.
|
13
8
|
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
super(options)
|
18
|
-
DefaultLogger.debug("MessagePusher#initialize(options = {})") if options[:env] == "development"
|
19
|
-
|
20
|
-
queues_string = options[:queues].map { |name, type| "#{name} (key:#{type})" }.join(",")
|
21
|
-
DefaultLogger.info "================================================================"
|
22
|
-
DefaultLogger.info "Booting #{self.class.name} (#{options[:env]})"
|
23
|
-
DefaultLogger.info "Exchange: #{options[:exchange]} (#{options[:type]} passive:#{options[:passive]} durable:#{options[:durable]})"
|
24
|
-
DefaultLogger.info "Queues: #{queues_string}"
|
25
|
-
DefaultLogger.info "amqp://#{options[:user]}:#{options[:pass]}@#{options[:host]}:#{options[:port]}#{options[:vhost]}"
|
26
|
-
DefaultLogger.info ""
|
27
|
-
end
|
9
|
+
class MessagePusher
|
10
|
+
include MessageCommand
|
11
|
+
include Amqp::Writer
|
28
12
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
13
|
+
##
|
14
|
+
# Initializes a new intance
|
15
|
+
#
|
16
|
+
# @param options<Hash> Options hash for the message pusher.
|
17
|
+
#
|
18
|
+
def initialize(options = {})
|
19
|
+
super(options)
|
20
|
+
DefaultLogger.debug("MessagePusher#initialize(options = {})") if options[:env] == "development"
|
37
21
|
|
38
|
-
|
39
|
-
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
22
|
+
queues_string = options[:queues].map { |name, type| "#{name} (key:#{type})" }.join(",")
|
23
|
+
DefaultLogger.info "================================================================"
|
24
|
+
DefaultLogger.info "Booting #{self.class.name} (#{options[:env]})"
|
25
|
+
DefaultLogger.info "Exchange: #{options[:exchange]} (#{options[:type]} passive:#{options[:passive]} durable:#{options[:durable]})"
|
26
|
+
DefaultLogger.info "Queues: #{queues_string}"
|
27
|
+
DefaultLogger.info "amqp://#{options[:user]}:#{options[:pass]}@#{options[:host]}:#{options[:port]}#{options[:vhost]}"
|
28
|
+
DefaultLogger.info ""
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Initialize the AMQP server connection and exchange so we can write messages to the queue.
|
33
|
+
#
|
34
|
+
def initialize_machine
|
35
|
+
DefaultLogger.debug("MessagePusher#initialize_machine") if options[:env] == "development"
|
36
|
+
initialize_writer
|
37
|
+
initialize_queues
|
38
|
+
end
|
39
|
+
|
40
|
+
def description
|
41
|
+
"exchange:#{options[:exchange]}"
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Handle the messages by writing them into the AMQP server exchange.
|
46
|
+
#
|
47
|
+
# @param message<Hash> The message to write to the exchange.
|
48
|
+
#
|
49
|
+
def process_standard(message)
|
50
|
+
DefaultLogger.debug("MessagePusher#process_standard(message)") if options[:env] == "development"
|
51
|
+
publish(message)
|
52
|
+
acknowledge(message) if options[:ack]
|
53
|
+
end
|
46
54
|
|
47
|
-
##
|
48
|
-
# Handle the messages by writing them into the AMQP server exchange.
|
49
|
-
#
|
50
|
-
# @param message<Hash> The message to write to the exchange.
|
51
|
-
#
|
52
|
-
def process_standard(message)
|
53
|
-
DefaultLogger.debug("MessagePusher#process_standard(message)") if options[:env] == "development"
|
54
|
-
publish(message)
|
55
|
-
acknowledge(message) if options[:ack]
|
56
55
|
end
|
57
56
|
|
58
57
|
end
|
@@ -3,228 +3,235 @@ require "mq"
|
|
3
3
|
require "time"
|
4
4
|
require "socket"
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
#
|
9
|
-
#
|
10
|
-
|
6
|
+
module PipelineToolkit
|
7
|
+
##
|
8
|
+
# The message subscriber is used to subscribe to a AMQP server queue
|
9
|
+
# and handle a single message at a time.
|
10
|
+
#
|
11
|
+
class MessageSubscriber
|
11
12
|
|
12
|
-
|
13
|
+
include Amqp::Reader
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
attr_reader :start_time
|
16
|
+
attr_reader :mps
|
17
|
+
attr_reader :options
|
18
|
+
attr_accessor :structure
|
18
19
|
|
19
|
-
|
20
|
+
PIPE_PATH = "/tmp"
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
DefaultLogger.init_logger(options)
|
29
|
-
|
30
|
-
@options = options
|
22
|
+
##
|
23
|
+
# Initializes a new instance
|
24
|
+
#
|
25
|
+
# @param options<Hash> An options hash, see command line interface.
|
26
|
+
#
|
27
|
+
def initialize(options = {})
|
31
28
|
|
32
|
-
|
33
|
-
@options[:name] ||= (Socket.gethostname + '_' + Process.pid.to_s)
|
34
|
-
|
35
|
-
DefaultLogger.info "================================================================"
|
36
|
-
DefaultLogger.info "Booting #{self.class.name} (#{options[:env]})"
|
37
|
-
DefaultLogger.info "Exchange: #{options[:exchange]} (#{options[:type]} passive:#{options[:passive]} durable:#{options[:durable]})"
|
38
|
-
DefaultLogger.info "Queue: #{options[:queue]} (ack:#{options[:ack]})"
|
39
|
-
DefaultLogger.info "amqp://#{options[:user]}:#{options[:pass]}@#{options[:host]}:#{options[:port]}#{options[:vhost]}"
|
40
|
-
DefaultLogger.info "Monitoring: http://localhost:#{options[:http_port]}/ (#{options[:content_type]} name:#{options[:name]} DNS-SD:#{options[:dnssd]})"
|
41
|
-
DefaultLogger.info ""
|
42
|
-
|
43
|
-
@queues_out = ''
|
29
|
+
DefaultLogger.init_logger(options)
|
44
30
|
|
45
|
-
|
31
|
+
@options = options
|
32
|
+
options[:ack] = !options[:no_acks]
|
46
33
|
|
47
|
-
|
48
|
-
|
34
|
+
DefaultLogger.info "================================================================"
|
35
|
+
DefaultLogger.info "Booting #{self.class.name} (#{options[:env]})"
|
36
|
+
DefaultLogger.info "Exchange: #{options[:exchange]} (#{options[:type]} passive:#{options[:passive]} durable:#{options[:durable]})"
|
37
|
+
DefaultLogger.info "Queue: #{options[:queue]} (ack:#{options[:ack]})"
|
38
|
+
DefaultLogger.info "amqp://#{options[:user]}:#{options[:pass]}@#{options[:host]}:#{options[:port]}#{options[:vhost]}"
|
39
|
+
DefaultLogger.info "Monitoring: http://localhost:#{options[:http_port]}/ (#{options[:content_type]})"
|
40
|
+
DefaultLogger.info ""
|
41
|
+
|
42
|
+
@structure = [description]
|
43
|
+
|
44
|
+
reset_message_statistics
|
45
|
+
end
|
49
46
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
47
|
+
##
|
48
|
+
# Starts the subscriber reactor loop
|
49
|
+
#
|
50
|
+
def start
|
54
51
|
|
55
|
-
|
56
|
-
|
52
|
+
Signal.trap('INT') { EM.stop }
|
53
|
+
Signal.trap('TERM') { EM.stop }
|
57
54
|
|
58
|
-
|
59
|
-
|
60
|
-
|
55
|
+
DefaultLogger.debug("MessageSubscriber#start")
|
56
|
+
begin
|
57
|
+
create_sys_pipe
|
61
58
|
|
62
|
-
|
63
|
-
|
59
|
+
EM.run do
|
60
|
+
@start_time = Time.now
|
64
61
|
|
65
|
-
|
66
|
-
|
62
|
+
initialize_reader
|
63
|
+
queue_subscribe
|
67
64
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
65
|
+
conn = EM.watch(@sys_pipe, Handlers::MessageHandler, self, options)
|
66
|
+
# must call this to setup callback to notify_readable
|
67
|
+
conn.notify_readable = true
|
68
|
+
|
69
|
+
if monitoring_enabled?
|
70
|
+
EM.start_server('0.0.0.0', options[:http_port], Monitoring::MonitorServer, self, options)
|
71
|
+
EM.add_periodic_timer(5) { calculate_message_statistics }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
rescue StandardError => e
|
75
|
+
DefaultLogger.error "#{e.class.name}: #{e.message}\n" << e.backtrace.join("\n")
|
76
|
+
raise e
|
77
|
+
ensure
|
78
|
+
shutdown
|
74
79
|
end
|
75
|
-
rescue StandardError => e
|
76
|
-
DefaultLogger.error "#{e.class.name}: #{e.message}\n" << e.backtrace.join("\n")
|
77
|
-
raise e
|
78
|
-
ensure
|
79
|
-
shutdown
|
80
80
|
end
|
81
|
-
end
|
82
81
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
end
|
91
|
-
|
92
|
-
##
|
93
|
-
# Callback for the Handlers::MessageHandler when it receives a message
|
94
|
-
#
|
95
|
-
# @param message<Hash> The decoded message
|
96
|
-
#
|
97
|
-
def process(message)
|
98
|
-
case message[:msg_type]
|
99
|
-
when :ack
|
100
|
-
perform_acknowledgement(message[:ack_id])
|
101
|
-
when :pipe_desc
|
102
|
-
DefaultLogger.debug("AcknowledgementHandler#queues_out")
|
103
|
-
@queues_out = message[:queues]
|
104
|
-
else
|
105
|
-
raise Exception.new("Unknown control message received: #{message}")
|
82
|
+
##
|
83
|
+
# Stop the subscriber
|
84
|
+
#
|
85
|
+
def shutdown
|
86
|
+
DefaultLogger.info("Shutting down #{self.class.name}")
|
87
|
+
DefaultLogger.info "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"
|
88
|
+
destroy_sys_pipe
|
106
89
|
end
|
107
|
-
end
|
108
|
-
|
109
|
-
##
|
110
|
-
# Support templating of member data.
|
111
|
-
#
|
112
|
-
def get_binding
|
113
|
-
binding
|
114
|
-
end
|
115
|
-
|
116
|
-
# :nodoc:
|
117
|
-
def name
|
118
|
-
options[:name] || ''
|
119
|
-
end
|
120
|
-
|
121
|
-
# :nodoc:
|
122
|
-
def queue_name
|
123
|
-
options[:name] || ''
|
124
|
-
end
|
125
|
-
|
126
|
-
protected
|
127
90
|
|
128
|
-
|
129
|
-
|
130
|
-
#
|
131
|
-
# @param header<MQ::Header> The header of the message
|
132
|
-
# @param body Description
|
133
|
-
#
|
134
|
-
def process_queue_message(header, body)
|
135
|
-
DefaultLogger.debug("MessageSubscriber#process_queue_message(header, body)")
|
136
|
-
body = MessageCoder.decode(body)
|
137
|
-
unless body.is_a?(Hash)
|
138
|
-
message = { :msg_type => :standard, :raw => body }
|
139
|
-
else
|
140
|
-
message = { :msg_type => :standard }.merge(body)
|
91
|
+
def monitoring_enabled?
|
92
|
+
!options[:http_port].nil?
|
141
93
|
end
|
142
|
-
|
143
|
-
write(message)
|
144
|
-
end
|
145
|
-
|
146
|
-
private
|
147
|
-
|
94
|
+
|
148
95
|
##
|
149
|
-
#
|
96
|
+
# Callback for the Handlers::MessageHandler when it receives a message
|
150
97
|
#
|
151
|
-
|
152
|
-
|
153
|
-
|
98
|
+
# @param message<Hash> The decoded message
|
99
|
+
#
|
100
|
+
def process(message)
|
101
|
+
case message[:msg_type]
|
102
|
+
when "ack"
|
103
|
+
DefaultLogger.debug("Acknowledging message #{message[:ack_id]}")
|
104
|
+
perform_acknowledgement(message[:ack_id])
|
105
|
+
when "pipe_desc"
|
106
|
+
@structure << message[:description].to_s
|
107
|
+
else
|
108
|
+
raise "Unknown control message received: #{message.inspect}"
|
109
|
+
end
|
154
110
|
end
|
155
|
-
|
111
|
+
|
156
112
|
##
|
157
|
-
#
|
113
|
+
# Support templating of member data.
|
158
114
|
#
|
159
|
-
def
|
160
|
-
|
161
|
-
@mps = @count / @time_delta
|
162
|
-
reset_message_statistics
|
115
|
+
def get_binding
|
116
|
+
binding
|
163
117
|
end
|
164
|
-
|
118
|
+
|
119
|
+
# :nodoc:
|
120
|
+
def process_id
|
121
|
+
Process.pid.to_s
|
122
|
+
end
|
123
|
+
|
165
124
|
##
|
166
|
-
#
|
125
|
+
# The uptime of the subscriber in mins
|
167
126
|
#
|
168
|
-
def
|
169
|
-
|
170
|
-
`mkfifo #{name}`
|
171
|
-
@ack_pipe = File.new(name, "r+")
|
172
|
-
message = { :msg_type => :system,
|
173
|
-
:sys_pipe => name,
|
174
|
-
:ack => options[:ack] }
|
175
|
-
write_to_pipe(message)
|
127
|
+
def uptime
|
128
|
+
(Time.now - self.start_time).to_i
|
176
129
|
end
|
177
130
|
|
178
131
|
##
|
179
|
-
#
|
132
|
+
# The hostname of the server the subscriber is running on
|
180
133
|
#
|
181
|
-
def
|
182
|
-
|
134
|
+
def hostname
|
135
|
+
Socket.gethostname
|
183
136
|
end
|
184
137
|
|
138
|
+
def description
|
139
|
+
"queue:#{options[:queue]}"
|
140
|
+
end
|
141
|
+
|
142
|
+
protected
|
143
|
+
|
185
144
|
##
|
186
|
-
#
|
187
|
-
# Stolen from EM.
|
145
|
+
# Process the message received from AMQP server
|
188
146
|
#
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
147
|
+
# @param header<MQ::Header> The header of the message
|
148
|
+
# @param body Description
|
149
|
+
#
|
150
|
+
def process_queue_message(header, body)
|
151
|
+
DefaultLogger.debug("MessageSubscriber#process_queue_message(header, body)")
|
152
|
+
body = MessageCoder.decode(body)
|
153
|
+
DefaultLogger.debug("--------------- #{body.inspect}")
|
154
|
+
unless body.is_a?(Hash)
|
155
|
+
message = { :msg_type => 'standard', :raw => body }
|
156
|
+
else
|
157
|
+
message = { :msg_type => 'standard' }.merge(body)
|
158
|
+
end
|
159
|
+
store_acknowledgement(message, header) if options[:ack]
|
160
|
+
write(message)
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
##
|
166
|
+
# Reset the instance variables used to store message statistics
|
167
|
+
#
|
168
|
+
def reset_message_statistics
|
169
|
+
@count = 0
|
170
|
+
@prev_time = Time.now
|
171
|
+
end
|
172
|
+
|
173
|
+
##
|
174
|
+
# Calculate the statistics on message handling
|
175
|
+
#
|
176
|
+
def calculate_message_statistics
|
177
|
+
@time_delta = Time.now - @prev_time
|
178
|
+
@mps = @count / @time_delta
|
179
|
+
reset_message_statistics
|
194
180
|
end
|
195
181
|
|
196
|
-
|
197
|
-
|
198
|
-
|
182
|
+
##
|
183
|
+
# Create the acknowledgement named pipe
|
184
|
+
#
|
185
|
+
def create_sys_pipe
|
186
|
+
name = File.join(PIPE_PATH, "sys_pipe_#{generate_guid}")
|
187
|
+
`mkfifo #{name}`
|
188
|
+
@sys_pipe = File.new(name, "r+")
|
189
|
+
message = { :msg_type => "system",
|
190
|
+
:sys_pipe => name,
|
191
|
+
:ack => options[:ack] }
|
192
|
+
write_to_pipe(message)
|
193
|
+
end
|
194
|
+
|
195
|
+
##
|
196
|
+
# Destroy the acknowledgement named pipe
|
197
|
+
#
|
198
|
+
def destroy_sys_pipe
|
199
|
+
`rm #{@sys_pipe.path}`
|
200
|
+
end
|
199
201
|
|
200
|
-
|
201
|
-
|
202
|
+
##
|
203
|
+
# Generates a unique guid to use as part of the acknowledgement named pipe name.
|
204
|
+
# Stolen from EM.
|
205
|
+
#
|
206
|
+
def generate_guid
|
207
|
+
# Cache uuidgen seed for better performance
|
208
|
+
if @ix and @ix >= 10_000
|
209
|
+
@ix = nil
|
210
|
+
@seed = nil
|
211
|
+
end
|
212
|
+
|
213
|
+
# NB. This will only work on *nix platforms
|
214
|
+
@seed ||= `uuidgen`.chomp.gsub(/-/,"")
|
215
|
+
@ix ||= 0
|
216
|
+
|
217
|
+
"#{@seed}#{@ix += 1}"
|
218
|
+
end
|
202
219
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
220
|
+
##
|
221
|
+
# Write the message to STDOUT
|
222
|
+
#
|
223
|
+
# @param message<Hash> The message to write
|
224
|
+
#
|
225
|
+
def write(message)
|
226
|
+
DefaultLogger.debug("MessageSubscriber#write(#{message.inspect})")
|
227
|
+
@count += 1
|
228
|
+
write_to_pipe(message)
|
229
|
+
end
|
213
230
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
def initialize_dnssd
|
218
|
-
return unless options[:dnssd]
|
219
|
-
# FIXME. DNS-SD breaks when under load. Not sure what problem is yet, have raised it with
|
220
|
-
# gem author:
|
221
|
-
# http://github.com/tenderlove/dnssd/issues#issue/3
|
222
|
-
require 'dnssd'
|
223
|
-
DNSSD.register!("#{options[:name]}_pipe", "_http._tcp", nil, options[:http_port])
|
224
|
-
end
|
231
|
+
def write_to_pipe(message, pipe=$stdout)
|
232
|
+
pipe.syswrite(MessageCoder.encode(message) << "\n")
|
233
|
+
end
|
225
234
|
|
226
|
-
|
227
|
-
pipe.syswrite(MessageCoder.encode(message) << "\n")
|
228
|
-
end
|
235
|
+
end
|
229
236
|
|
230
|
-
end
|
237
|
+
end
|