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.
- data/Gemfile +3 -0
- data/Gemfile.lock +12 -0
- data/README.rdoc +9 -0
- data/VERSION +1 -1
- data/bin/minecraftctl +13 -4
- data/bin/minecraftctlserver +89 -195
- data/lib/message_collector.rb +21 -0
- data/lib/minecraft.rb +281 -0
- data/lib/spawn.rb +78 -0
- data/minecraftctl.gemspec +16 -5
- data/spec/message_collector_spec.rb +61 -0
- data/spec/minecraft_spec.rb +80 -0
- data/spec/minecraftctlserver_spec.rb +92 -0
- data/spec/spec_helper.rb +0 -1
- data/spec/stub_server/minecraft +100 -0
- metadata +43 -6
- data/lib/minecraftctl.rb +0 -0
- data/spec/minecraftctl_spec.rb +0 -7
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -2,9 +2,13 @@ GEM
|
|
2
2
|
remote: http://rubygems.org/
|
3
3
|
specs:
|
4
4
|
arrayfields (4.7.4)
|
5
|
+
cgi_multipart_eof_fix (2.5.0)
|
5
6
|
chronic (0.6.4)
|
7
|
+
daemons (1.1.4)
|
6
8
|
diff-lcs (1.1.3)
|
9
|
+
fastthread (1.0.1)
|
7
10
|
fattr (2.2.0)
|
11
|
+
gem_plugin (0.2.3)
|
8
12
|
git (1.2.5)
|
9
13
|
haml (3.1.3)
|
10
14
|
httpclient (2.2.1)
|
@@ -18,6 +22,12 @@ GEM
|
|
18
22
|
fattr (~> 2.2.0)
|
19
23
|
map (~> 4.3.0)
|
20
24
|
map (4.3.0)
|
25
|
+
mongrel (1.1.5)
|
26
|
+
cgi_multipart_eof_fix (>= 2.4)
|
27
|
+
daemons (>= 1.0.3)
|
28
|
+
fastthread (>= 1.0.1)
|
29
|
+
gem_plugin (>= 0.2.3)
|
30
|
+
open4 (1.1.0)
|
21
31
|
rack (1.3.2)
|
22
32
|
rake (0.9.2)
|
23
33
|
rcov (0.9.10)
|
@@ -55,6 +65,8 @@ DEPENDENCIES
|
|
55
65
|
httpclient (>= 2.2.1)
|
56
66
|
jeweler (~> 1.6.4)
|
57
67
|
main (>= 4.7.3)
|
68
|
+
mongrel (>= 1.1.5)
|
69
|
+
open4 (>= 1.1.0)
|
58
70
|
rcov
|
59
71
|
reek (~> 1.2.8)
|
60
72
|
roodi (~> 2.1.0)
|
data/README.rdoc
CHANGED
@@ -12,6 +12,15 @@ And control it with 'minecraftctl', for help try:
|
|
12
12
|
|
13
13
|
$ minecraftctl serverhelp
|
14
14
|
|
15
|
+
== Changelog
|
16
|
+
|
17
|
+
=== v1.1.0
|
18
|
+
* Output from server is streamed in real time
|
19
|
+
* Fixed problem with initial pid file creation
|
20
|
+
* Massive refactoring and tests
|
21
|
+
* Added 'status' command
|
22
|
+
* Better error detection and reporting
|
23
|
+
|
15
24
|
== Contributing to minecraftctl
|
16
25
|
|
17
26
|
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.1.0
|
data/bin/minecraftctl
CHANGED
@@ -2,18 +2,22 @@
|
|
2
2
|
require 'rubygems'
|
3
3
|
require 'main'
|
4
4
|
require 'httpclient'
|
5
|
+
require 'thread'
|
6
|
+
Thread.abort_on_exception = true
|
5
7
|
|
6
8
|
Main do
|
7
9
|
description 'controls Minecraft via minecraftctlserver'
|
8
|
-
|
10
|
+
|
9
11
|
option 'server', 's' do
|
10
12
|
default 'localhost'
|
11
13
|
description 'minecraftctlserver address'
|
14
|
+
argument_required
|
12
15
|
end
|
13
16
|
|
14
17
|
option 'port', 'p' do
|
15
18
|
default 25560
|
16
19
|
description 'minecraftctlserver port'
|
20
|
+
argument_required
|
17
21
|
end
|
18
22
|
|
19
23
|
argument 'command' do
|
@@ -38,13 +42,18 @@ Main do
|
|
38
42
|
|
39
43
|
args = params['arguments'].values
|
40
44
|
|
41
|
-
if ['log', 'inspect', 'list', 'help'].include? command
|
42
|
-
|
45
|
+
if ['status', 'log', 'inspect', 'list', 'help'].include? command
|
46
|
+
c.get_async("http://#{params['server'].value}:#{params['port'].value}/#{command}").pop.content.each do |line|
|
47
|
+
puts line
|
48
|
+
end
|
43
49
|
else
|
44
|
-
|
50
|
+
c.post_async("http://#{params['server'].value}:#{params['port'].value}/#{command}", args.join("\n")).pop.content.each do |line|
|
51
|
+
puts line
|
52
|
+
end
|
45
53
|
end
|
46
54
|
rescue Errno::ECONNREFUSED => e
|
47
55
|
puts "Falied to connect to minecraftctlserver; please run minecraftctlserver: #{e}"
|
48
56
|
end
|
49
57
|
end
|
50
58
|
end
|
59
|
+
|
data/bin/minecraftctlserver
CHANGED
@@ -5,181 +5,37 @@ require 'open3'
|
|
5
5
|
require 'thread'
|
6
6
|
require 'pathname'
|
7
7
|
|
8
|
-
|
9
|
-
def initialize(msg)
|
10
|
-
@msg = msg
|
11
|
-
end
|
12
|
-
|
13
|
-
attr_reader :msg
|
8
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
9
|
|
15
|
-
|
16
|
-
|
17
|
-
end
|
10
|
+
require 'minecraft'
|
11
|
+
require 'message_collector'
|
18
12
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
13
|
+
class MessageCollectorProcessed < MessageCollector
|
14
|
+
def initialize(&operations)
|
15
|
+
super(&operations)
|
16
|
+
@processors = []
|
23
17
|
end
|
24
18
|
|
25
|
-
|
19
|
+
def process(&block)
|
20
|
+
@processors << block
|
21
|
+
self
|
26
22
|
end
|
27
23
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
attr_reader :time, :level, :msg
|
34
|
-
|
35
|
-
def to_s
|
36
|
-
@msg
|
24
|
+
def collect(msg)
|
25
|
+
@processors.each do |processor|
|
26
|
+
msg = processor.call(msg)
|
37
27
|
end
|
28
|
+
@collector.call(msg) if msg
|
38
29
|
end
|
39
30
|
end
|
40
31
|
|
41
|
-
class
|
42
|
-
def initialize(
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
@running = false
|
48
|
-
|
49
|
-
@history = []
|
50
|
-
@recent = []
|
51
|
-
end
|
52
|
-
|
53
|
-
def running?
|
54
|
-
@running
|
55
|
-
end
|
56
|
-
|
57
|
-
def start
|
58
|
-
collect_msg do
|
59
|
-
if @running
|
60
|
-
internal_msg "Server already running"
|
61
|
-
else
|
62
|
-
time_operation("Server start") do
|
63
|
-
@stdin, stdout, stderr = Open3.popen3(@cmd)
|
64
|
-
@running = true
|
65
|
-
|
66
|
-
@out_reader = Thread.new do
|
67
|
-
stdout.each do |line|
|
68
|
-
puts "OUT: #{line}"
|
69
|
-
@out_queue << Message::Out.new(line)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
@err_reader = Thread.new do
|
74
|
-
stderr.each do |line|
|
75
|
-
puts "ERR: #{line}"
|
76
|
-
@out_queue << Message::Err.new(line)
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
wait_msg do |m|
|
81
|
-
m.msg =~ /Done \(([^n]*)ns\)!/
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
32
|
+
class TextCollector < MessageCollectorProcessed
|
33
|
+
def initialize(&operations)
|
34
|
+
super
|
35
|
+
process do |msg|
|
36
|
+
msg.to_s + "\n"
|
85
37
|
end
|
86
38
|
end
|
87
|
-
|
88
|
-
def stop
|
89
|
-
unless @running
|
90
|
-
return collect_msg do
|
91
|
-
internal_msg "Server already stopped"
|
92
|
-
end
|
93
|
-
else
|
94
|
-
return command('stop') do
|
95
|
-
@running = false
|
96
|
-
time_operation("Server stop") do
|
97
|
-
@out_reader.join
|
98
|
-
@err_reader.join
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def save_all
|
105
|
-
command('save-all') do
|
106
|
-
time_operation("Save") do
|
107
|
-
wait_msg{|m| m.msg =~ /Save complete/}
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
def list
|
113
|
-
command('list') do
|
114
|
-
wait_msg{|m| m.msg =~ /Connected players:/}
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
def command(cmd)
|
119
|
-
raise RuntimeError, "server not running" unless @running
|
120
|
-
collect_msg do
|
121
|
-
@stdin.write("#{cmd}\n")
|
122
|
-
if block_given?
|
123
|
-
yield
|
124
|
-
else
|
125
|
-
active_wait
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def history
|
131
|
-
archive_msg
|
132
|
-
@history
|
133
|
-
end
|
134
|
-
|
135
|
-
private
|
136
|
-
|
137
|
-
def flush_msg
|
138
|
-
until @out_queue.empty?
|
139
|
-
@recent << @out_queue.pop(true)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def archive_msg
|
144
|
-
flush_msg
|
145
|
-
recent = @recent.dup
|
146
|
-
@history += @recent
|
147
|
-
@recent.clear
|
148
|
-
recent
|
149
|
-
end
|
150
|
-
|
151
|
-
def collect_msg
|
152
|
-
archive_msg
|
153
|
-
yield
|
154
|
-
archive_msg
|
155
|
-
end
|
156
|
-
|
157
|
-
def internal_msg(msg)
|
158
|
-
@out_queue << Message::Internal.new(msg)
|
159
|
-
end
|
160
|
-
|
161
|
-
def time_operation(name)
|
162
|
-
start = Time.now
|
163
|
-
yield
|
164
|
-
internal_msg "#{name} finished in #{(Time.now - start).to_f}"
|
165
|
-
end
|
166
|
-
|
167
|
-
def wait_msg(discard = false)
|
168
|
-
loop do
|
169
|
-
msg = @out_queue.pop
|
170
|
-
if yield msg
|
171
|
-
@recent << msg unless discard
|
172
|
-
break
|
173
|
-
end
|
174
|
-
@recent << msg
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def active_wait
|
179
|
-
@stdin.write("list\n")
|
180
|
-
wait_msg(true){|m| m.msg =~ /Connected players:/}
|
181
|
-
end
|
182
|
-
|
183
39
|
end
|
184
40
|
|
185
41
|
class Daemon
|
@@ -188,7 +44,7 @@ class Daemon
|
|
188
44
|
Process.setsid # become session leader
|
189
45
|
exit if fork # and exits
|
190
46
|
# now in child
|
191
|
-
|
47
|
+
|
192
48
|
# try to lock before we kill stdin/out
|
193
49
|
lock(pid_file)
|
194
50
|
|
@@ -205,7 +61,7 @@ class Daemon
|
|
205
61
|
end
|
206
62
|
|
207
63
|
def self.lock(pid_file)
|
208
|
-
pf = File.open(pid_file,
|
64
|
+
pf = File.open(pid_file, File::RDWR | File::CREAT)
|
209
65
|
fail "Server already running with pid: #{pf.read}" unless pf.flock(File::LOCK_EX|File::LOCK_NB)
|
210
66
|
pf.truncate(0)
|
211
67
|
pf.write(Process.pid.to_s)
|
@@ -219,11 +75,13 @@ Main do
|
|
219
75
|
option 'command', 'c' do
|
220
76
|
default 'java -Xms256M -Xmx512M -Djava.net.preferIPv4Stack=true -jar minecraft_server.jar nogui'
|
221
77
|
description 'command to be used to span server'
|
78
|
+
argument_required
|
222
79
|
end
|
223
80
|
|
224
81
|
option 'port', 'p' do
|
225
82
|
default 25560
|
226
83
|
description 'port on which the control HTTP server should be running'
|
84
|
+
argument_required
|
227
85
|
end
|
228
86
|
|
229
87
|
option 'foreground', 'f' do
|
@@ -233,15 +91,18 @@ Main do
|
|
233
91
|
option 'pid-file', 'P' do
|
234
92
|
description 'pid file relative to minecraft-dir'
|
235
93
|
default 'minecraftctlserver.pid'
|
94
|
+
argument_required
|
236
95
|
end
|
237
96
|
|
238
97
|
option 'log-file', 'l' do
|
239
98
|
description 'log file relative to minecraft-dir when in not in foreground'
|
240
99
|
default 'minecraftctlserver.log'
|
100
|
+
argument_required
|
241
101
|
end
|
242
102
|
|
243
103
|
argument 'minecraft-dir' do
|
244
104
|
description 'directory path to mincraft server installation directory'
|
105
|
+
argument_required
|
245
106
|
end
|
246
107
|
|
247
108
|
run do
|
@@ -265,59 +126,77 @@ Main do
|
|
265
126
|
|
266
127
|
s = Sinatra.new
|
267
128
|
s.set :port, params['port'].value
|
129
|
+
s.set :environment, 'production'
|
130
|
+
s.set :server, ['mongrel']
|
131
|
+
s.set :lock, true
|
268
132
|
|
269
133
|
s.post '/save-all' do
|
270
|
-
|
271
|
-
|
134
|
+
TextCollector.for(minecraft) do
|
135
|
+
save_all
|
136
|
+
end
|
272
137
|
end
|
273
138
|
|
274
139
|
s.post '/start' do
|
275
|
-
|
276
|
-
|
140
|
+
TextCollector.for(minecraft) do
|
141
|
+
start
|
142
|
+
end
|
277
143
|
end
|
278
144
|
|
279
145
|
s.post '/stop' do
|
280
|
-
|
281
|
-
|
146
|
+
TextCollector.for(minecraft) do
|
147
|
+
stop
|
148
|
+
end
|
282
149
|
end
|
283
150
|
|
284
151
|
s.post '/shutdown' do
|
285
|
-
|
286
|
-
|
287
|
-
pid = Process.pid
|
288
|
-
Thread.new{ sleep 1; Process.kill(15, pid)}
|
152
|
+
TextCollector.for(minecraft) do
|
153
|
+
stop
|
289
154
|
|
290
|
-
|
155
|
+
pid = Process.pid
|
156
|
+
Thread.new{ sleep 1; Process.kill(15, pid)}
|
157
|
+
end
|
291
158
|
end
|
292
159
|
|
293
160
|
s.post %r{/(.+)} do |cmd|
|
294
161
|
args = request.body.read.split("\n")
|
295
|
-
|
296
|
-
|
162
|
+
TextCollector.for(minecraft) do
|
163
|
+
send(cmd.tr('-', '_').to_sym, *args)
|
164
|
+
end
|
297
165
|
end
|
298
166
|
|
299
167
|
s.get '/help' do
|
300
168
|
start = false
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
169
|
+
TextCollector.for(minecraft) do
|
170
|
+
help
|
171
|
+
log "status show server status"
|
172
|
+
log "log show recent server messages"
|
173
|
+
log "inspect inspect recent server messages"
|
174
|
+
end.process do |msg|
|
175
|
+
msg = msg.to_s
|
176
|
+
|
177
|
+
if msg =~ /Console commands:/
|
306
178
|
start = true
|
307
|
-
next
|
179
|
+
next
|
308
180
|
end
|
309
|
-
end.
|
310
|
-
map{|m| m.sub(/help or \?/, 'serverhelp ')}.
|
311
|
-
map{|m| m.sub(/^( |\t)*/, '')}
|
312
|
-
@msg << "log show recent server messages"
|
313
|
-
@msg << "inspect inspect recent server messages"
|
314
181
|
|
315
|
-
|
182
|
+
next unless start
|
183
|
+
msg.sub(/help or \?/, 'serverhelp ').sub(/^( |\t)*/, '')
|
184
|
+
end
|
316
185
|
end
|
317
186
|
|
318
187
|
s.get '/list' do
|
319
|
-
|
320
|
-
|
188
|
+
TextCollector.for(minecraft) do
|
189
|
+
list
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
s.get '/status' do
|
194
|
+
if minecraft.running?
|
195
|
+
@pid = minecraft.server_pid
|
196
|
+
haml :status_running
|
197
|
+
else
|
198
|
+
haml :status_stopped
|
199
|
+
end
|
321
200
|
end
|
322
201
|
|
323
202
|
s.get '/inspect' do
|
@@ -330,9 +209,12 @@ Main do
|
|
330
209
|
haml :messages
|
331
210
|
end
|
332
211
|
|
333
|
-
s.
|
334
|
-
|
335
|
-
|
212
|
+
s.template :status_running do
|
213
|
+
'= "Minecraft server is running with pid: #{@pid}"'
|
214
|
+
end
|
215
|
+
|
216
|
+
s.template :status_stopped do
|
217
|
+
'= "Minecraft server is stopped"'
|
336
218
|
end
|
337
219
|
|
338
220
|
s.template :messages do
|
@@ -343,6 +225,18 @@ Main do
|
|
343
225
|
'= @msg.map{|m| m.inspect}.join("\n")'
|
344
226
|
end
|
345
227
|
|
228
|
+
s.not_found do
|
229
|
+
"Request '#{env['REQUEST_PATH']}' not supported, please see '/serverhelp' for details\n"
|
230
|
+
end
|
231
|
+
|
232
|
+
s.error do
|
233
|
+
"Error in minecraftctlserver while processing request '#{env['REQUEST_PATH']}': #{env['sinatra.error']}\n"
|
234
|
+
end
|
235
|
+
|
346
236
|
s.run!
|
237
|
+
|
238
|
+
# make sure we stop the server on exit
|
239
|
+
minecraft.stop if minecraft.running?
|
347
240
|
end
|
348
241
|
end
|
242
|
+
|