tty-process-ctl 0.4.0 → 0.5.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/Gemfile CHANGED
@@ -1,10 +1,11 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  group :development do
4
- gem "rspec", "~> 2.8"
5
- gem "rdoc", "~> 3.12"
6
- gem "bundler", "~> 1.1"
7
- gem "jeweler", "~> 1.8"
8
- gem "simplecov", ">= 0"
9
- gem "cli", "~> 1.0"
4
+ gem "rake", "~> 0.9"
5
+ gem "rspec", "~> 2.8"
6
+ gem "rdoc", "~> 3.12"
7
+ gem "bundler", "~> 1.1"
8
+ gem "jeweler", "~> 1.8"
9
+ gem "simplecov", ">= 0"
10
+ gem "cli", "~> 1.0"
10
11
  end
@@ -11,6 +11,7 @@ GEM
11
11
  rdoc
12
12
  json (1.7.5)
13
13
  multi_json (1.3.6)
14
+ rake (0.9.2.2)
14
15
  rdoc (3.12)
15
16
  json (~> 1.4)
16
17
  rspec (2.11.0)
@@ -33,6 +34,7 @@ DEPENDENCIES
33
34
  bundler (~> 1.1)
34
35
  cli (~> 1.0)
35
36
  jeweler (~> 1.8)
37
+ rake (~> 0.9)
36
38
  rdoc (~> 3.12)
37
39
  rspec (~> 2.8)
38
40
  simplecov
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 0.5.0
@@ -4,18 +4,43 @@ require 'pty'
4
4
  require 'io/console'
5
5
 
6
6
  class TTYProcessCtl
7
- class Timeout < Timeout::Error
7
+ Timeout = Class.new(Timeout::Error)
8
+
9
+ class Listener
10
+ def initialize(&callback)
11
+ @callback = callback
12
+ end
13
+
14
+ def call(message)
15
+ @callback.call(message)
16
+ rescue LocalJumpError => error
17
+ # brake in listener
18
+ close if error.reason == :break
19
+ end
20
+
21
+ def on_close(&callback)
22
+ @on_close = callback unless closed?
23
+ self
24
+ end
25
+
26
+ def close
27
+ @on_close.call(self) if @on_close
28
+ @closed = true
29
+ end
30
+
31
+ def closed?
32
+ @closed
33
+ end
8
34
  end
9
35
 
10
36
  include Enumerable
11
37
 
12
38
  def initialize(command, options = {})
13
- @max_queue_length = options[:max_queue_length] || 4000
14
- @max_messages = options[:max_messages] || 4000
39
+ @backlog_size = options[:backlog_size] || 4000
15
40
  @command = command
16
41
 
42
+ @listeners = []
17
43
  @out_queue = Queue.new
18
- @messages = []
19
44
 
20
45
  @r, @w, @pid = PTY.spawn(@command)
21
46
  @w.echo = false # disable echoing of commands
@@ -47,18 +72,26 @@ class TTYProcessCtl
47
72
  raise IOError.new("process '#{@command}' (pid: #{@pid}) not accepting input")
48
73
  end
49
74
 
50
- def messages
51
- @messages
75
+ def on(regexp = nil, &callback)
76
+ # return listender to user so he can close it after use
77
+ listener do |message|
78
+ next if regexp and message !~ regexp
79
+ callback.call(message)
80
+ end
52
81
  end
53
82
 
54
- def each(options = {})
55
- return enum_for(:each, options) unless block_given?
56
- timeout(options[:timeout]) do
57
- while !@out_queue.empty? or alive? do
58
- yield (dequeue or break)
83
+ def each(options = {}, &block)
84
+ return enum_for(:each, options) unless block
85
+ listener = listener(&block)
86
+ begin
87
+ timeout(options[:timeout]) do
88
+ true while not listener.closed? and poll
59
89
  end
90
+ self
91
+ ensure
92
+ # make sure we close the listener when each exits
93
+ listener.close
60
94
  end
61
- self
62
95
  end
63
96
 
64
97
  def each_until(pattern, options = {})
@@ -84,17 +117,25 @@ class TTYProcessCtl
84
117
  end
85
118
 
86
119
  def wait_exit(options = {})
87
- each(options){}
120
+ poll!(options)
88
121
  @thread.join
89
122
  self
90
123
  end
91
124
 
92
- def flush
93
- loop do
94
- dequeue(true)
125
+ def poll(options = {})
126
+ timeout(options[:timeout]) do
127
+ return process_message
95
128
  end
96
- self
97
- rescue ThreadError
129
+ end
130
+
131
+ def poll!(options = {})
132
+ timeout(options[:timeout]) do
133
+ true while process_message
134
+ end
135
+ end
136
+
137
+ def flush
138
+ true while process_message_no_block
98
139
  self
99
140
  end
100
141
 
@@ -108,17 +149,34 @@ class TTYProcessCtl
108
149
  end
109
150
  end
110
151
 
111
- def dequeue(no_block = false)
152
+ def listener(&block)
153
+ listener = Listener.new(&block).on_close do |listener|
154
+ @listeners.delete(listener)
155
+ end
156
+ @listeners << listener
157
+ listener
158
+ end
159
+
160
+ def process_message_no_block
161
+ process_message(true)
162
+ rescue ThreadError
163
+ nil
164
+ end
165
+
166
+ def process_message(no_block = false)
167
+ return nil if not alive? and @out_queue.empty?
112
168
  message = @out_queue.pop(no_block)
113
169
  return nil unless message
114
- @messages << message
115
- @messages.pop while @messages.length > @max_messages
170
+ message.freeze
171
+ @listeners.each do |listener|
172
+ listener.call(message)
173
+ end
116
174
  message
117
175
  end
118
176
 
119
177
  def enqueue_message(message)
120
178
  @out_queue << message
121
- @out_queue.pop while @out_queue.length > @max_queue_length
179
+ @out_queue.pop while @out_queue.length > @backlog_size
122
180
  end
123
181
 
124
182
  def enqueue_end
@@ -5,31 +5,45 @@ describe TTYProcessCtl do
5
5
  TTYProcessCtl.new('spec/stub')
6
6
  end
7
7
 
8
- describe 'process output enumeration' do
9
- subject do
10
- TTYProcessCtl.new('spec/stub --exit')
11
- end
8
+ after :each do
9
+ subject.send_command 'stop' if subject.alive?
10
+ subject.wait_exit
11
+ end
12
+
13
+ it 'should be Enumerable' do
14
+ subject.should respond_to :take
15
+ subject.take(2).should == ["151 recipes", "16 achievements"]
16
+ end
17
+
18
+ it 'should skip oldest messages if backlog queue is full' do
19
+ subject = TTYProcessCtl.new('spec/stub', backlog_size: 2)
12
20
 
21
+ subject.each_until(/Done/).to_a
22
+ subject.send_command 'help'
23
+ subject.send_command 'stop'
24
+
25
+ # fait for backlog to overflow
26
+ sleep 0.2
27
+
28
+ subject.each.to_a.should == [
29
+ "2011-09-19 22:12:00 [INFO] Saving chunks",
30
+ "2011-09-19 22:12:00 [INFO] Saving chunks"
31
+ ]
32
+ end
33
+
34
+ describe 'process output enumeration' do
13
35
  it 'should allow iterating the output lines' do
36
+ subject.send_command 'stop'
14
37
  lines_count = 0
15
38
  subject.each do |line|
16
39
  lines_count += 1
17
40
  end
18
- lines_count.should == 20
41
+ lines_count.should == 23
19
42
  end
20
43
 
21
44
  it 'should allow iterating the output lines with enumerator' do
22
- subject.each.to_a.length.should == 20
23
- end
24
-
25
- it 'should be Enumerable' do
26
- subject.should respond_to :take
27
- subject.take(2).should == ["151 recipes", "16 achievements"]
28
- end
29
-
30
- it 'should return nothing if iterating on dead process' do
31
- subject.each.to_a.length.should == 20
32
- subject.each.to_a.should be_empty
45
+ subject.send_command 'stop'
46
+ subject.each.to_a.length.should == 23
33
47
  end
34
48
 
35
49
  it 'should allow iteration until pattern is found in message' do
@@ -42,8 +56,132 @@ describe TTYProcessCtl do
42
56
 
43
57
  it 'should allow waiting for message matching pattern' do
44
58
  subject.wait_until(/NOT ENOUGH RAM/)
59
+ subject.send_command 'stop'
45
60
  subject.each.to_a.first.should == "2011-09-10 12:58:55 [WARNING] To start the server with more ram, launch it as \"java -Xmx1024M -Xms1024M -jar minecraft_server.jar\""
46
61
  end
62
+
63
+ it 'should return nothing if iterating on dead process' do
64
+ subject.send_command 'stop'
65
+ subject.each.to_a.length.should == 23
66
+ subject.should_not be_alive
67
+ subject.each.to_a.should be_empty
68
+ end
69
+
70
+ it 'should allow flushing backlog messages' do
71
+ subject.each_until(/SERVER IS RUNNING/).to_a
72
+ sleep 0.2
73
+
74
+ subject.flush
75
+
76
+ subject.send_command 'list'
77
+ subject.each_until(/Connected players/).to_a.should == ["2011-09-20 14:42:04 [INFO] Connected players: kazuya"]
78
+ end
79
+ end
80
+
81
+ describe 'on message callbacks' do
82
+ describe 'when enumerating' do
83
+ it 'should call on message callback' do
84
+ messages = []
85
+ subject.on do |message|
86
+ messages << message
87
+ end
88
+
89
+ subject.wait_until(/NOT ENOUGH RAM/)
90
+ messages.should == [
91
+ "151 recipes",
92
+ "16 achievements",
93
+ "2011-09-10 12:58:55 [INFO] Starting minecraft server version Beta 1.7.3",
94
+ "2011-09-10 12:58:55 [WARNING] **** NOT ENOUGH RAM!"
95
+ ]
96
+ end
97
+
98
+ it 'should call on message callback if given regexp matches the message' do
99
+ messages = []
100
+ subject.on(/recipes|achievements/) do |message|
101
+ messages << message
102
+ end
103
+
104
+ subject.wait_until(/NOT ENOUGH RAM/)
105
+ messages.should == [
106
+ "151 recipes",
107
+ "16 achievements"
108
+ ]
109
+ end
110
+ end
111
+
112
+ describe 'when polling' do
113
+ it 'should call on message callback' do
114
+ messages = []
115
+ subject.on do |message|
116
+ messages << message
117
+ end
118
+
119
+ subject.send_command 'stop'
120
+ subject.poll!(timeout: 1.0)
121
+
122
+ messages.length.should == 23
123
+ end
124
+
125
+ it 'should call on message callback if given regexp matches the message' do
126
+ messages = []
127
+ subject.on(/recipes|achievements/) do |message|
128
+ messages << message
129
+ end
130
+
131
+ subject.send_command 'stop'
132
+ subject.poll!(timeout: 1.0)
133
+
134
+ messages.should == [
135
+ "151 recipes",
136
+ "16 achievements"
137
+ ]
138
+ end
139
+ end
140
+
141
+ describe 'when flushing' do
142
+ it 'should call on message callback if given regexp matches the message' do
143
+ messages = []
144
+ subject.on(/recipes|achievements/) do |message|
145
+ messages << message
146
+ end
147
+
148
+ sleep 1.0
149
+ subject.flush
150
+
151
+ messages.should == [
152
+ "151 recipes",
153
+ "16 achievements"
154
+ ]
155
+ end
156
+ end
157
+
158
+ describe 'closing' do
159
+ it 'with close method on listener' do
160
+ counter = 0
161
+ listener = subject.on do
162
+ counter += 1
163
+ end
164
+
165
+ subject.poll
166
+ listener.close
167
+ subject.poll
168
+
169
+ counter.should == 1
170
+ end
171
+
172
+ it 'with break' do
173
+ counter = 0
174
+ subject.on do
175
+ counter += 1
176
+ break
177
+ end
178
+
179
+ subject.poll
180
+ subject.poll
181
+
182
+ counter.should == 1
183
+ end
184
+ end
47
185
  end
48
186
 
49
187
  describe 'sending commands' do
@@ -76,35 +214,6 @@ describe TTYProcessCtl do
76
214
  end
77
215
  end
78
216
 
79
- describe 'messages' do
80
- subject do
81
- TTYProcessCtl.new('spec/stub --exit')
82
- end
83
-
84
- it 'should allow access to previously outputed messages' do
85
- subject.each.to_a
86
- subject.messages.length.should == 20
87
- end
88
-
89
- describe 'flushing' do
90
- subject do
91
- TTYProcessCtl.new('spec/stub')
92
- end
93
-
94
- it 'should allow flushing queued messages before iteration' do
95
- subject.each_until(/SERVER IS RUNNING/).to_a
96
- sleep 0.2
97
-
98
- subject.flush
99
-
100
- subject.send_command 'list'
101
- subject.each_until(/Connected players/).to_a.should == ["2011-09-20 14:42:04 [INFO] Connected players: kazuya"]
102
-
103
- subject.send_command 'stop'
104
- end
105
- end
106
- end
107
-
108
217
  describe 'process status query' do
109
218
  it 'should allow querying if process is alive' do
110
219
  subject.should be_alive
@@ -130,60 +239,34 @@ describe TTYProcessCtl do
130
239
  end
131
240
  end
132
241
 
133
- describe 'limiting' do
134
- it 'should allow defining maximum number of messages that can be queued' do
135
- subject = TTYProcessCtl.new('spec/stub', max_queue_length: 2)
136
-
137
- subject.each_until(/Done/).to_a
138
- subject.send_command 'help'
139
- subject.send_command 'stop'
140
-
141
- sleep 0.2
142
- subject.each.to_a.length.should == 2
143
- subject.wait_exit
144
- end
145
-
146
- it 'should allow defining maximum number of messages that can be remembered' do
147
- subject = TTYProcessCtl.new('spec/stub', max_messages: 2)
148
-
149
- subject.each_until(/Done/).to_a
150
- subject.send_command 'help'
151
- subject.flush
152
- subject.send_command 'stop'
153
- subject.wait_exit
154
-
155
- subject.messages.length.should == 2
156
- end
157
- end
158
-
159
242
  describe 'timeout' do
160
- after :each do
161
- subject.send_command 'stop' if subject.alive?
162
- subject.wait_exit
243
+ subject do
244
+ # wait for process to be ready and delay each message printout by 0.1 second
245
+ TTYProcessCtl.new('spec/stub --delay 0.01').wait_until(/151 recipes/, timeout: 1)
163
246
  end
164
247
 
165
248
  describe 'each calls with block' do
166
249
  it 'should raise TTYProcessCtl::Timeout on timieout' do
167
250
  expect {
168
- subject.each(timeout: 0.1) {}
251
+ subject.each(timeout: 0.1){}
169
252
  }.to raise_error TTYProcessCtl::Timeout
170
253
 
171
254
  expect {
172
- subject.each_until(/bogous/, timeout: 0.1) {}
255
+ subject.each_until(/bogous/, timeout: 0.1){}
173
256
  }.to raise_error TTYProcessCtl::Timeout
174
257
 
175
258
  expect {
176
- subject.each_until_exclude(/bogous/, timeout: 0.1) {}
259
+ subject.each_until_exclude(/bogous/, timeout: 0.1){}
177
260
  }.to raise_error TTYProcessCtl::Timeout
178
261
  end
179
262
 
180
263
  it 'should not raise error if they return before timeout' do
181
264
  expect {
182
- subject.each_until(/recipes/, timeout: 1) {}
265
+ subject.each_until(/achievements/, timeout: 1){}
183
266
  }.to_not raise_error TTYProcessCtl::Timeout
184
267
 
185
268
  expect {
186
- subject.each_until_exclude(/achievements/, timeout: 1) {}
269
+ subject.each_until_exclude(/NOT ENOUGH RAM/, timeout: 1){}
187
270
  }.to_not raise_error TTYProcessCtl::Timeout
188
271
 
189
272
  expect {
@@ -209,11 +292,11 @@ describe TTYProcessCtl do
209
292
 
210
293
  it 'should not raise error if they return before timeout' do
211
294
  expect {
212
- subject.each_until(/recipes/, timeout: 1).to_a
295
+ subject.each_until(/achievements/, timeout: 1).to_a
213
296
  }.to_not raise_error TTYProcessCtl::Timeout
214
297
 
215
298
  expect {
216
- subject.each_until_exclude(/achievements/, timeout: 1).to_a
299
+ subject.each_until_exclude(/NOT ENOUGH RAM/, timeout: 1).to_a
217
300
  }.to_not raise_error TTYProcessCtl::Timeout
218
301
 
219
302
  expect {
@@ -248,17 +331,15 @@ describe TTYProcessCtl do
248
331
  end
249
332
 
250
333
  describe 'chaining' do
251
- subject do
252
- TTYProcessCtl.new('spec/stub --exit')
253
- end
254
-
255
334
  it 'should work with each methods' do
335
+ subject.send_command 'stop'
256
336
  subject.each_until(/recipes/){}.should == subject
257
337
  subject.each_until_exclude(/achievements/){}.should == subject
258
338
  subject.each{}.should == subject
259
339
  end
260
340
 
261
341
  it 'should work with wait methods' do
342
+ subject.send_command 'stop'
262
343
  subject.wait_until(/Done/){}.should == subject
263
344
  subject.wait_exit{}.should == subject
264
345
  end
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "tty-process-ctl"
8
- s.version = "0.4.0"
8
+ s.version = "0.5.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Jakub Pastuszek"]
12
- s.date = "2012-11-18"
12
+ s.date = "2012-11-24"
13
13
  s.description = "This gem was created to enable control of interactive terminal applications. It is using pseudo tty to communicate with the process via simple API."
14
14
  s.email = "jpastuszek@gmail.com"
15
15
  s.extra_rdoc_files = [
@@ -44,6 +44,7 @@ Gem::Specification.new do |s|
44
44
  s.specification_version = 3
45
45
 
46
46
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
47
+ s.add_development_dependency(%q<rake>, ["~> 0.9"])
47
48
  s.add_development_dependency(%q<rspec>, ["~> 2.8"])
48
49
  s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
49
50
  s.add_development_dependency(%q<bundler>, ["~> 1.1"])
@@ -51,6 +52,7 @@ Gem::Specification.new do |s|
51
52
  s.add_development_dependency(%q<simplecov>, [">= 0"])
52
53
  s.add_development_dependency(%q<cli>, ["~> 1.0"])
53
54
  else
55
+ s.add_dependency(%q<rake>, ["~> 0.9"])
54
56
  s.add_dependency(%q<rspec>, ["~> 2.8"])
55
57
  s.add_dependency(%q<rdoc>, ["~> 3.12"])
56
58
  s.add_dependency(%q<bundler>, ["~> 1.1"])
@@ -59,6 +61,7 @@ Gem::Specification.new do |s|
59
61
  s.add_dependency(%q<cli>, ["~> 1.0"])
60
62
  end
61
63
  else
64
+ s.add_dependency(%q<rake>, ["~> 0.9"])
62
65
  s.add_dependency(%q<rspec>, ["~> 2.8"])
63
66
  s.add_dependency(%q<rdoc>, ["~> 3.12"])
64
67
  s.add_dependency(%q<bundler>, ["~> 1.1"])
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tty-process-ctl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-18 00:00:00.000000000 Z
12
+ date: 2012-11-24 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70125546618580 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '0.9'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70125546618580
14
25
  - !ruby/object:Gem::Dependency
15
26
  name: rspec
16
- requirement: &70168846879920 !ruby/object:Gem::Requirement
27
+ requirement: &70125546616420 !ruby/object:Gem::Requirement
17
28
  none: false
18
29
  requirements:
19
30
  - - ~>
@@ -21,10 +32,10 @@ dependencies:
21
32
  version: '2.8'
22
33
  type: :development
23
34
  prerelease: false
24
- version_requirements: *70168846879920
35
+ version_requirements: *70125546616420
25
36
  - !ruby/object:Gem::Dependency
26
37
  name: rdoc
27
- requirement: &70168846879440 !ruby/object:Gem::Requirement
38
+ requirement: &70125546629980 !ruby/object:Gem::Requirement
28
39
  none: false
29
40
  requirements:
30
41
  - - ~>
@@ -32,10 +43,10 @@ dependencies:
32
43
  version: '3.12'
33
44
  type: :development
34
45
  prerelease: false
35
- version_requirements: *70168846879440
46
+ version_requirements: *70125546629980
36
47
  - !ruby/object:Gem::Dependency
37
48
  name: bundler
38
- requirement: &70168846878880 !ruby/object:Gem::Requirement
49
+ requirement: &70125546627680 !ruby/object:Gem::Requirement
39
50
  none: false
40
51
  requirements:
41
52
  - - ~>
@@ -43,10 +54,10 @@ dependencies:
43
54
  version: '1.1'
44
55
  type: :development
45
56
  prerelease: false
46
- version_requirements: *70168846878880
57
+ version_requirements: *70125546627680
47
58
  - !ruby/object:Gem::Dependency
48
59
  name: jeweler
49
- requirement: &70168846878320 !ruby/object:Gem::Requirement
60
+ requirement: &70125546623080 !ruby/object:Gem::Requirement
50
61
  none: false
51
62
  requirements:
52
63
  - - ~>
@@ -54,10 +65,10 @@ dependencies:
54
65
  version: '1.8'
55
66
  type: :development
56
67
  prerelease: false
57
- version_requirements: *70168846878320
68
+ version_requirements: *70125546623080
58
69
  - !ruby/object:Gem::Dependency
59
70
  name: simplecov
60
- requirement: &70168846877760 !ruby/object:Gem::Requirement
71
+ requirement: &70125546577740 !ruby/object:Gem::Requirement
61
72
  none: false
62
73
  requirements:
63
74
  - - ! '>='
@@ -65,10 +76,10 @@ dependencies:
65
76
  version: '0'
66
77
  type: :development
67
78
  prerelease: false
68
- version_requirements: *70168846877760
79
+ version_requirements: *70125546577740
69
80
  - !ruby/object:Gem::Dependency
70
81
  name: cli
71
- requirement: &70168846877040 !ruby/object:Gem::Requirement
82
+ requirement: &70125546572940 !ruby/object:Gem::Requirement
72
83
  none: false
73
84
  requirements:
74
85
  - - ~>
@@ -76,7 +87,7 @@ dependencies:
76
87
  version: '1.0'
77
88
  type: :development
78
89
  prerelease: false
79
- version_requirements: *70168846877040
90
+ version_requirements: *70125546572940
80
91
  description: This gem was created to enable control of interactive terminal applications.
81
92
  It is using pseudo tty to communicate with the process via simple API.
82
93
  email: jpastuszek@gmail.com
@@ -117,7 +128,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
128
  version: '0'
118
129
  segments:
119
130
  - 0
120
- hash: 1843831823515240226
131
+ hash: -3155130806959499412
121
132
  required_rubygems_version: !ruby/object:Gem::Requirement
122
133
  none: false
123
134
  requirements: