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 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
+