minecraftctl 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -16,4 +16,7 @@ group :development do
16
16
  gem "main", ">= 4.7.3"
17
17
  gem "haml", ">= 3.1.3"
18
18
  gem "httpclient", ">= 2.2.1"
19
+ gem "open4", ">= 1.1.0"
20
+ gem "mongrel", ">= 1.1.5"
19
21
  end
22
+
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.0.0
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
- puts c.get_content("http://#{params['server'].value}:#{params['port'].value}/#{command}")
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
- puts c.post_content("http://#{params['server'].value}:#{params['port'].value}/#{command}", args.join("\n"))
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
+
@@ -5,181 +5,37 @@ require 'open3'
5
5
  require 'thread'
6
6
  require 'pathname'
7
7
 
8
- class Message
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
- def to_s
16
- @msg
17
- end
10
+ require 'minecraft'
11
+ require 'message_collector'
18
12
 
19
- class Internal < Message
20
- def initialize(msg)
21
- super(' -- ' + msg)
22
- end
13
+ class MessageCollectorProcessed < MessageCollector
14
+ def initialize(&operations)
15
+ super(&operations)
16
+ @processors = []
23
17
  end
24
18
 
25
- class Out < Message
19
+ def process(&block)
20
+ @processors << block
21
+ self
26
22
  end
27
23
 
28
- class Err < Message
29
- def initialize(line)
30
- x, @time, @level, @msg = *line.match(/^([^ ]* [^ ]*) \[([^\]]*)\] (.*)/)
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 Minecraft
42
- def initialize(cmd)
43
- @cmd = cmd
44
- @in_queue = Queue.new
45
- @out_queue = Queue.new
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, 'r+')
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
- @msg = minecraft.save_all
271
- haml :messages
134
+ TextCollector.for(minecraft) do
135
+ save_all
136
+ end
272
137
  end
273
138
 
274
139
  s.post '/start' do
275
- @msg = minecraft.start
276
- haml :messages
140
+ TextCollector.for(minecraft) do
141
+ start
142
+ end
277
143
  end
278
144
 
279
145
  s.post '/stop' do
280
- @msg = minecraft.stop
281
- haml :messages
146
+ TextCollector.for(minecraft) do
147
+ stop
148
+ end
282
149
  end
283
150
 
284
151
  s.post '/shutdown' do
285
- @msg = minecraft.stop
286
-
287
- pid = Process.pid
288
- Thread.new{ sleep 1; Process.kill(15, pid)}
152
+ TextCollector.for(minecraft) do
153
+ stop
289
154
 
290
- haml :messages
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
- @msg = minecraft.command(cmd + " " + args.join(' '))
296
- haml :messages
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
- @msg = minecraft.command('help').
302
- map{|m| m.msg}.
303
- select do |m|
304
- next true if start
305
- if m =~ /Console commands:/
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 false
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
- haml :messages
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
- @msg = minecraft.list
320
- haml :messages
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.get %r{/(.+)} do |cmd|
334
- @msg = minecraft.command(cmd)
335
- haml :messages
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
+