minecraftctl 1.0.0 → 1.1.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.
@@ -0,0 +1,21 @@
1
+ class MessageCollector
2
+ def initialize(&operation)
3
+ @operation = operation
4
+ end
5
+
6
+ def self.for(minecraft, &operations)
7
+ self.new do |collector|
8
+ minecraft.with_message_collector(collector, &operations)
9
+ end
10
+ end
11
+
12
+ def collect(msg)
13
+ @collector.call(msg)
14
+ end
15
+
16
+ def each(&collector)
17
+ @collector = collector
18
+ @operation.call(method(:collect))
19
+ end
20
+ end
21
+
data/lib/minecraft.rb ADDED
@@ -0,0 +1,281 @@
1
+ require 'timeout'
2
+ require 'spawn'
3
+ Thread.abort_on_exception = true
4
+
5
+ class Minecraft
6
+ class HistoryQueue < Queue
7
+ def initialize
8
+ @history = []
9
+ super
10
+ end
11
+
12
+ def flush
13
+ until empty?
14
+ msg = pop(true)
15
+ yield msg if block_given?
16
+ end
17
+ end
18
+
19
+ def history
20
+ flush
21
+ @history
22
+ end
23
+
24
+ def pop(blocking = false)
25
+ msg = super(blocking)
26
+ @history << msg
27
+ msg
28
+ end
29
+ end
30
+
31
+ class MessageQueue < HistoryQueue
32
+ class Message
33
+ def initialize(msg)
34
+ @msg = msg
35
+ end
36
+
37
+ attr_reader :msg
38
+
39
+ def to_s
40
+ @msg
41
+ end
42
+
43
+ class Internal < Message
44
+ def initialize(msg)
45
+ super(msg)
46
+ end
47
+ end
48
+
49
+ class InternalError < Message
50
+ def initialize(msg)
51
+ super(msg)
52
+ end
53
+ end
54
+
55
+ class Out < Message
56
+ def initialize(msg)
57
+ super(msg)
58
+ end
59
+ end
60
+
61
+ class Err < Message
62
+ def initialize(line)
63
+ x, @time, @level, msg = *line.match(/^([^ ]* [^ ]*) \[([^\]]*)\] (.*)/)
64
+ msg = line unless @time and @level and msg
65
+ super(msg)
66
+ end
67
+
68
+ attr_reader :time, :level
69
+ end
70
+ end
71
+
72
+ def initialize
73
+ super
74
+ end
75
+
76
+ def out(msg)
77
+ push Message::Out.new(msg)
78
+ end
79
+
80
+ def err(msg)
81
+ push Message::Err.new(msg)
82
+ end
83
+
84
+ def log(msg)
85
+ push Message::Internal.new(msg)
86
+ end
87
+
88
+ def error(msg)
89
+ push Message::InternalError.new(msg)
90
+ end
91
+ end
92
+
93
+ class StartupFailedError < RuntimeError
94
+ def initialize(command)
95
+ super "failed to start process: #{command}"
96
+ end
97
+ end
98
+
99
+ def initialize(cmd)
100
+ @cmd = cmd
101
+ @in_queue = Queue.new
102
+ @message_queue = MessageQueue.new
103
+
104
+ @server_pid = nil
105
+
106
+ @collector = nil
107
+ end
108
+
109
+ attr_reader :server_pid
110
+
111
+ def with_message_collector(collector, &operations)
112
+ @message_queue.flush
113
+ @collector = collector
114
+ begin
115
+ instance_eval &operations
116
+ rescue Timeout::Error
117
+ error "Command timed out"
118
+ ensure
119
+ @message_queue.flush do |msg|
120
+ collect(msg)
121
+ end
122
+ @collector = nil
123
+ end
124
+ end
125
+
126
+ def running?
127
+ @out_reader and @in_writter.alive?
128
+ end
129
+
130
+ def log(msg)
131
+ @message_queue.log(msg)
132
+ end
133
+
134
+ def error(msg)
135
+ @message_queue.error(msg)
136
+ end
137
+
138
+ def start
139
+ if running?
140
+ log "Server already running"
141
+ else
142
+ time_operation("Server start") do
143
+ begin
144
+ log "Starting minecraft: #{@cmd}"
145
+
146
+ pid, @stdin, @stdout, @stderr = Spawn::spawn(@cmd)
147
+ @server_pid = pid
148
+
149
+ log "Started server process with pid: #{@server_pid}"
150
+
151
+ @in_writter = Thread.new do
152
+ begin
153
+ loop do
154
+ msg = @in_queue.pop
155
+ @stdin.write(msg)
156
+ end
157
+ rescue IOError, Errno::EPIPE
158
+ end
159
+ end
160
+
161
+ @err_reader = Thread.new do
162
+ begin
163
+ @stderr.each do |line|
164
+ @message_queue.err(line.strip)
165
+ end
166
+ rescue IOError
167
+ end
168
+ end
169
+
170
+ @out_reader = Thread.new do
171
+ begin
172
+ @stdout.each do |line|
173
+ @message_queue.out(line.strip)
174
+ end
175
+ rescue IOError
176
+ ensure
177
+ @in_writter.kill
178
+ @err_reader.kill
179
+
180
+ @stdin.close unless @stdin.closed?
181
+ @stderr.close unless @stderr.closed?
182
+ @stdout.close unless @stdout.closed?
183
+
184
+ log "Minecraft exits"
185
+ end
186
+ end
187
+
188
+ wait_msg do |m|
189
+ m.msg =~ /Done \(([^n]*)ns\)!/ or m.msg =~ /Minecraft exits/
190
+ end
191
+
192
+ unless running?
193
+ Process.wait(@server_pid)
194
+ raise StartupFailedError, @cmd
195
+ end
196
+ rescue Errno::ENOENT
197
+ raise StartupFailedError, @cmd
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ def stop
204
+ unless running?
205
+ log "Server already stopped"
206
+ else
207
+ command('stop') do
208
+ time_operation("Server stop") do
209
+ Process.wait(@server_pid)
210
+ log "Server stopped"
211
+ end
212
+
213
+ wait_msg{|m| m.msg == "Server stopped"}
214
+ end
215
+ end
216
+ end
217
+
218
+ def save_all
219
+ command('save-all') do
220
+ time_operation("Save") do
221
+ wait_msg{|m| m.msg =~ /Save complete/}
222
+ end
223
+ end
224
+ end
225
+
226
+ def list
227
+ command('list') do
228
+ wait_msg{|m| m.msg =~ /Connected players:/}
229
+ end
230
+ end
231
+
232
+ def method_missing(m, *args)
233
+ command(([m.to_s.tr('_', '-')] + args).join(' '))
234
+ end
235
+
236
+ def command(cmd)
237
+ raise RuntimeError, "server not running" unless running?
238
+ @in_queue << "#{cmd}\n"
239
+ if block_given?
240
+ yield
241
+ else
242
+ active_wait
243
+ end
244
+ end
245
+
246
+ def history
247
+ @message_queue.history
248
+ end
249
+
250
+ private
251
+
252
+ def collect(msg)
253
+ @collector.call(msg) if @collector
254
+ end
255
+
256
+ def time_operation(name)
257
+ start = Time.now
258
+ yield
259
+ log "#{name} finished in #{(Time.now - start).to_f}"
260
+ end
261
+
262
+ def wait_msg(discard = false, timeout = 20)
263
+ Timeout::timeout(timeout) do
264
+ loop do
265
+ msg = @message_queue.pop
266
+ if yield msg
267
+ collect(msg) unless discard
268
+ break
269
+ end
270
+
271
+ collect(msg)
272
+ end
273
+ end
274
+ end
275
+
276
+ def active_wait
277
+ @in_queue << "list\n"
278
+ wait_msg(true){|m| m.msg =~ /Connected players:/}
279
+ end
280
+ end
281
+
data/lib/spawn.rb ADDED
@@ -0,0 +1,78 @@
1
+ require 'fcntl'
2
+
3
+ class Spawn
4
+ def self.spawn(*cmd, &b)
5
+ pw, pr, pe, ps = IO.pipe, IO.pipe, IO.pipe, IO.pipe
6
+
7
+ ps.first.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
8
+ ps.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
9
+
10
+ cid = fork {
11
+ pw.last.close
12
+ STDIN.reopen pw.first
13
+ pw.first.close
14
+
15
+ pr.first.close
16
+ STDOUT.reopen pr.last
17
+ pr.last.close
18
+
19
+ pe.first.close
20
+ STDERR.reopen pe.last
21
+ pe.last.close
22
+
23
+ STDOUT.sync = STDERR.sync = true
24
+
25
+ begin
26
+ # WARNING: detect max open fd no - is thre a better way?
27
+ r, w = IO.pipe
28
+ max_fd = r.to_i - 1
29
+ r.close
30
+ w.close
31
+
32
+ #puts "setting close on exec for fd's from 3 to #{max_fd}"
33
+ (3..max_fd).each do |fd|
34
+ begin
35
+ IO.for_fd(fd).fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
36
+ #puts "fd #{fd} set for closure on exec"
37
+ rescue Errno::EBADF
38
+ end
39
+ end
40
+
41
+ exec(*cmd)
42
+ raise 'forty-two'
43
+ rescue Exception => e
44
+ Marshal.dump(e, ps.last)
45
+ ps.last.flush
46
+ end
47
+ ps.last.close unless (ps.last.closed?)
48
+ exit!
49
+ }
50
+
51
+ [pw.first, pr.last, pe.last, ps.last].each{|fd| fd.close}
52
+
53
+ begin
54
+ e = Marshal.load ps.first
55
+ raise(Exception === e ? e : "unknown failure!")
56
+ rescue EOFError # If we get an EOF error, then the exec was successful
57
+ 42
58
+ ensure
59
+ ps.first.close
60
+ end
61
+
62
+ pw.last.sync = true
63
+
64
+ pi = [pw.last, pr.first, pe.first]
65
+
66
+ if b
67
+ begin
68
+ b[cid, *pi]
69
+ Process.waitpid2(cid).last
70
+ ensure
71
+ pi.each{|fd| fd.close unless fd.closed?}
72
+ end
73
+ else
74
+ [cid, pw.last, pr.first, pe.first]
75
+ end
76
+ end
77
+ end
78
+
data/minecraftctl.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "minecraftctl"
8
- s.version = "1.0.0"
8
+ s.version = "1.1.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 = "2011-09-10"
12
+ s.date = "2011-09-22"
13
13
  s.description = "Allows to send messages, start and stop Minecraft server"
14
14
  s.email = "jpastuszek@gmail.com"
15
15
  s.executables = ["minecraftctl", "minecraftctlserver", "minecraftctl", "minecraftctlserver"]
@@ -28,10 +28,15 @@ Gem::Specification.new do |s|
28
28
  "VERSION",
29
29
  "bin/minecraftctl",
30
30
  "bin/minecraftctlserver",
31
- "lib/minecraftctl.rb",
31
+ "lib/message_collector.rb",
32
+ "lib/minecraft.rb",
33
+ "lib/spawn.rb",
32
34
  "minecraftctl.gemspec",
33
- "spec/minecraftctl_spec.rb",
34
- "spec/spec_helper.rb"
35
+ "spec/message_collector_spec.rb",
36
+ "spec/minecraft_spec.rb",
37
+ "spec/minecraftctlserver_spec.rb",
38
+ "spec/spec_helper.rb",
39
+ "spec/stub_server/minecraft"
35
40
  ]
36
41
  s.homepage = "http://github.com/jpastuszek/minecraftctl"
37
42
  s.licenses = ["MIT"]
@@ -53,6 +58,8 @@ Gem::Specification.new do |s|
53
58
  s.add_development_dependency(%q<main>, [">= 4.7.3"])
54
59
  s.add_development_dependency(%q<haml>, [">= 3.1.3"])
55
60
  s.add_development_dependency(%q<httpclient>, [">= 2.2.1"])
61
+ s.add_development_dependency(%q<open4>, [">= 1.1.0"])
62
+ s.add_development_dependency(%q<mongrel>, [">= 1.1.5"])
56
63
  else
57
64
  s.add_dependency(%q<rspec>, ["~> 2.3.0"])
58
65
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
@@ -64,6 +71,8 @@ Gem::Specification.new do |s|
64
71
  s.add_dependency(%q<main>, [">= 4.7.3"])
65
72
  s.add_dependency(%q<haml>, [">= 3.1.3"])
66
73
  s.add_dependency(%q<httpclient>, [">= 2.2.1"])
74
+ s.add_dependency(%q<open4>, [">= 1.1.0"])
75
+ s.add_dependency(%q<mongrel>, [">= 1.1.5"])
67
76
  end
68
77
  else
69
78
  s.add_dependency(%q<rspec>, ["~> 2.3.0"])
@@ -76,6 +85,8 @@ Gem::Specification.new do |s|
76
85
  s.add_dependency(%q<main>, [">= 4.7.3"])
77
86
  s.add_dependency(%q<haml>, [">= 3.1.3"])
78
87
  s.add_dependency(%q<httpclient>, [">= 2.2.1"])
88
+ s.add_dependency(%q<open4>, [">= 1.1.0"])
89
+ s.add_dependency(%q<mongrel>, [">= 1.1.5"])
79
90
  end
80
91
  end
81
92
 
@@ -0,0 +1,61 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'message_collector'
3
+ require 'minecraft'
4
+
5
+ describe MessageCollector do
6
+ it 'should collect messages via passed proc call' do
7
+ mc = MessageCollector.new do |collector|
8
+ collector.call(:test1)
9
+ collector.call(:test2)
10
+ end
11
+
12
+ msgs = []
13
+ mc.each do |msg|
14
+ msgs << msg
15
+ end
16
+
17
+ msgs.should == [:test1, :test2]
18
+ end
19
+
20
+ describe 'with minecraft running' do
21
+ before :all do
22
+ @m = Minecraft.new(File.dirname(__FILE__) + '/stub_server/minecraft')
23
+ @m.start
24
+ end
25
+
26
+ it 'should collect messages generated by minecraft commands' do
27
+ msgs = []
28
+
29
+ MessageCollector.new do |collector|
30
+ @m.with_message_collector(collector) do
31
+ list
32
+ command(:help)
33
+ end
34
+ end.each do |msg|
35
+ msgs << msg.msg
36
+ end
37
+
38
+ msgs.first.should == 'Connected players: kazuya'
39
+ msgs.should include 'Console commands:'
40
+ end
41
+
42
+ it 'should provide #for method that shortens the code' do
43
+ msgs = []
44
+
45
+ MessageCollector.for(@m) do
46
+ list
47
+ command(:help)
48
+ end.each do |msg|
49
+ msgs << msg.msg
50
+ end
51
+
52
+ msgs.first.should == 'Connected players: kazuya'
53
+ msgs.should include 'Console commands:'
54
+ end
55
+
56
+ after :all do
57
+ @m.stop
58
+ end
59
+ end
60
+ end
61
+