tty-process-ctl 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: