473 lines
16 KiB
Python
473 lines
16 KiB
Python
# This file is part of the Minecraft Overviewer.
|
|
#
|
|
# Minecraft Overviewer is free software: you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or (at
|
|
# your option) any later version.
|
|
#
|
|
# Minecraft Overviewer is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
# Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with the Overviewer. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
from . import progressbar
|
|
from . import rcon
|
|
|
|
|
|
class Observer(object):
|
|
"""Base class that defines the observer interface.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._current_value = None
|
|
self._max_value = None
|
|
self.start_time = None
|
|
self.end_time = None
|
|
|
|
def start(self, max_value):
|
|
"""Signals the start of whatever process. Must be called before update
|
|
"""
|
|
self._set_max_value(max_value)
|
|
self.start_time = time.time()
|
|
self.update(0)
|
|
return self
|
|
|
|
def is_started(self):
|
|
return self.start_time is not None
|
|
|
|
def finish(self):
|
|
"""Signals the end of the processes, should be called after the
|
|
process is done.
|
|
"""
|
|
self.end_time = time.time()
|
|
|
|
def is_finished(self):
|
|
return self.end_time is not None
|
|
|
|
def is_running(self):
|
|
return self.is_started() and not self.is_finished()
|
|
|
|
def add(self, amount):
|
|
"""Shortcut to update by increments instead of absolute values. Zero
|
|
amounts are ignored.
|
|
"""
|
|
if amount:
|
|
self.update(self.get_current_value() + amount)
|
|
|
|
def update(self, current_value):
|
|
"""Set the progress value. Should be between 0 and max_value. Returns
|
|
whether this update is actually displayed.
|
|
"""
|
|
self._current_value = current_value
|
|
return False
|
|
|
|
def get_percentage(self):
|
|
"""Get the current progress percentage. Assumes 100% if max_value is 0
|
|
"""
|
|
if self.get_max_value() == 0:
|
|
return 100.0
|
|
else:
|
|
return self.get_current_value() * 100.0 / self.get_max_value()
|
|
|
|
def get_current_value(self):
|
|
return self._current_value
|
|
|
|
def get_max_value(self):
|
|
return self._max_value
|
|
|
|
def _set_max_value(self, max_value):
|
|
self._max_value = max_value
|
|
|
|
|
|
class LoggingObserver(Observer):
|
|
"""Simple observer that just outputs status through logging.
|
|
"""
|
|
def __init__(self):
|
|
super(Observer, self).__init__()
|
|
# this is an easy way to make the first update() call print a line
|
|
self.last_update = -101
|
|
|
|
# a fake ProgressBar, for the sake of ETA
|
|
class FakePBar(object):
|
|
def __init__(self):
|
|
self.maxval = None
|
|
self.currval = 0
|
|
self.finished = False
|
|
self.start_time = None
|
|
self.seconds_elapsed = 0
|
|
|
|
def finish(self):
|
|
self.update(self.maxval)
|
|
|
|
def update(self, value):
|
|
assert 0 <= value <= self.maxval
|
|
self.currval = value
|
|
if self.finished:
|
|
return False
|
|
if not self.start_time:
|
|
self.start_time = time.time()
|
|
self.seconds_elapsed = time.time() - self.start_time
|
|
|
|
if value == self.maxval:
|
|
self.finished = True
|
|
|
|
self.fake = FakePBar()
|
|
self.eta = progressbar.ETA()
|
|
|
|
def start(self, max_value):
|
|
self.fake.maxval = max_value
|
|
super(LoggingObserver, self).start(max_value)
|
|
|
|
def finish(self):
|
|
self.fake.finish()
|
|
logging.info("Rendered %d of %d. %d%% complete. %s",
|
|
self.get_max_value(), self.get_max_value(), 100.0,
|
|
self.eta.update(self.fake))
|
|
super(LoggingObserver, self).finish()
|
|
|
|
def update(self, current_value):
|
|
super(LoggingObserver, self).update(current_value)
|
|
self.fake.update(current_value)
|
|
|
|
if self._need_update():
|
|
logging.info("Rendered %d of %d. %d%% complete. %s",
|
|
self.get_current_value(), self.get_max_value(),
|
|
self.get_percentage(), self.eta.update(self.fake))
|
|
self.last_update = current_value
|
|
return True
|
|
return False
|
|
|
|
def _need_update(self):
|
|
cur_val = self.get_current_value()
|
|
if cur_val <= 100:
|
|
return cur_val - self.last_update >= 10
|
|
elif cur_val <= 500:
|
|
return cur_val - self.last_update >= 50
|
|
else:
|
|
return cur_val - self.last_update >= 100
|
|
|
|
|
|
default_widgets = [
|
|
progressbar.Percentage(), ' ',
|
|
progressbar.Bar(marker='=', left='[', right=']'), ' ',
|
|
progressbar.CounterWidget(), ' ',
|
|
progressbar.GenericSpeed(format='%.2ft/s'), ' ',
|
|
progressbar.ETA(prefix='eta ')
|
|
]
|
|
|
|
|
|
class ProgressBarObserver(progressbar.ProgressBar, Observer):
|
|
"""Display progress through a progressbar.
|
|
"""
|
|
|
|
# the progress bar is only updated in increments of this for performance
|
|
UPDATE_INTERVAL = 25
|
|
|
|
def __init__(self, widgets=default_widgets, term_width=None,
|
|
fd=sys.stderr):
|
|
super(ProgressBarObserver, self).__init__(widgets=widgets,
|
|
term_width=term_width, fd=fd)
|
|
self.last_update = 0 - (self.UPDATE_INTERVAL + 1)
|
|
|
|
def start(self, max_value):
|
|
self._set_max_value(max_value)
|
|
logging.info("Rendering %d total tiles." % max_value)
|
|
super(ProgressBarObserver, self).start()
|
|
|
|
def is_started(self):
|
|
return self.start_time is not None
|
|
|
|
def finish(self):
|
|
self._end_time = time.time()
|
|
super(ProgressBarObserver, self).finish()
|
|
self.fd.write('\n')
|
|
logging.info("Rendering complete!")
|
|
|
|
def update(self, current_value):
|
|
# maxval is an estimate, and progressbar barfs if currval > maxval
|
|
# so...
|
|
current_value = min(current_value, self.maxval)
|
|
if super(ProgressBarObserver, self).update(current_value):
|
|
self.last_update = self.get_current_value()
|
|
|
|
percentage = Observer.get_percentage
|
|
|
|
def get_current_value(self):
|
|
return self.currval
|
|
|
|
def get_max_value(self):
|
|
return self.maxval
|
|
|
|
def _set_max_value(self, max_value):
|
|
self.maxval = max_value
|
|
|
|
def _need_update(self):
|
|
return (self.get_current_value() - self.last_update
|
|
> self.UPDATE_INTERVAL)
|
|
|
|
|
|
class JSObserver(Observer):
|
|
"""Display progress on index.html using JavaScript
|
|
"""
|
|
|
|
def __init__(self, outputdir, minrefresh=5, messages=False):
|
|
"""Initialise observer
|
|
|
|
:outputdir: must be set to the map output directory path
|
|
:minrefresh: specifies the minimum gap between requests,
|
|
in seconds [optional]
|
|
:messages: is a dictionary which allows the displayed messages to
|
|
be customised [optional]
|
|
"""
|
|
self.last_update = -11
|
|
self.last_update_time = -1
|
|
self._current_value = -1
|
|
self.minrefresh = 1000 * minrefresh
|
|
self.json = dict()
|
|
|
|
# function to print formatted eta
|
|
self.format = lambda seconds: '%02ih %02im %02is' % \
|
|
(seconds // 3600, (seconds % 3600) // 60, seconds % 60)
|
|
|
|
if (not messages):
|
|
self.messages = dict(
|
|
totalTiles="Rendering %d tiles",
|
|
renderCompleted="Render completed in %02d:%02d:%02d",
|
|
renderProgress="Rendered %d of %d tiles (%d%% ETA:%s)")
|
|
elif (isinstance(messages, dict)):
|
|
if ('totalTiles' in messages
|
|
and 'renderCompleted' in messages
|
|
and 'renderProgress' in messages):
|
|
self.messages = messages
|
|
else:
|
|
raise Exception("JSObserver: messages parameter must be a "
|
|
"dictionary with three entries: totalTiles, "
|
|
"renderCompleted and renderProgress")
|
|
else:
|
|
raise Exception("JSObserver: messages parameter must be a "
|
|
"dictionary with three entries: totalTiles, "
|
|
"renderCompleted and renderProgress")
|
|
# On the initial render, the outputdir hasn't been created until after the observer is
|
|
# initialised, so let's just do it here if necessary.
|
|
if not os.path.exists(outputdir):
|
|
os.mkdir(outputdir)
|
|
|
|
self.logfile = open(os.path.join(outputdir, "progress.json"), "wb+", 0)
|
|
self.json["message"] = "Render starting..."
|
|
self.json["update"] = self.minrefresh
|
|
self.json["messageTime"] = time.time()
|
|
self.logfile.write(json.dumps(self.json).encode())
|
|
self.logfile.flush()
|
|
|
|
def start(self, max_value):
|
|
self.logfile.seek(0)
|
|
self.logfile.truncate()
|
|
self.json["message"] = self.messages["totalTiles"] % (max_value)
|
|
self.json["update"] = self.minrefresh
|
|
self.json["messageTime"] = time.time()
|
|
self.logfile.write(json.dumps(self.json).encode())
|
|
self.logfile.flush()
|
|
self.start_time = time.time()
|
|
self._set_max_value(max_value)
|
|
|
|
def is_started(self):
|
|
return self.start_time is not None
|
|
|
|
def finish(self):
|
|
"""Signals the end of the processes, should be called after the
|
|
process is done.
|
|
"""
|
|
self.end_time = time.time()
|
|
duration = self.end_time - self.start_time
|
|
self.logfile.seek(0)
|
|
self.logfile.truncate()
|
|
hours = duration // 3600
|
|
duration = duration % 3600
|
|
minutes = duration // 60
|
|
seconds = duration % 60
|
|
self.json["message"] = self.messages["renderCompleted"] \
|
|
% (hours, minutes, seconds)
|
|
# The 'renderCompleted' message will always be visible
|
|
# (until the next render)
|
|
self.json["update"] = 60000
|
|
self.json["messageTime"] = time.time()
|
|
self.logfile.write(json.dumps(self.json).encode())
|
|
self.logfile.close()
|
|
|
|
def is_finished(self):
|
|
return self.end_time is not None
|
|
|
|
def is_running(self):
|
|
return self.is_started() and not self.is_finished()
|
|
|
|
def add(self, amount):
|
|
"""Shortcut to update by increments instead of absolute values. Zero
|
|
amounts are ignored.
|
|
"""
|
|
if amount:
|
|
self.update(self.get_current_value() + amount)
|
|
|
|
def update(self, current_value):
|
|
"""Set the progress value. Should be between 0 and max_value. Returns
|
|
whether this update is actually displayed.
|
|
"""
|
|
self._current_value = current_value
|
|
if self._need_update():
|
|
refresh = max(1500 * (time.time() - max(self.start_time,
|
|
self.last_update_time)),
|
|
self.minrefresh) // 1
|
|
self.logfile.seek(0)
|
|
self.logfile.truncate()
|
|
if self.get_current_value():
|
|
duration = time.time() - self.start_time
|
|
eta = self.format(duration * self.get_max_value() /
|
|
self.get_current_value() - duration)
|
|
else:
|
|
eta = "?"
|
|
self.json["message"] = self.messages["renderProgress"] \
|
|
% (self.get_current_value(), self.get_max_value(),
|
|
self.get_percentage(), str(eta))
|
|
self.json["update"] = refresh
|
|
self.json["messageTime"] = time.time()
|
|
self.logfile.write(json.dumps(self.json).encode())
|
|
self.logfile.flush()
|
|
self.last_update_time = time.time()
|
|
self.last_update = current_value
|
|
return True
|
|
return False
|
|
|
|
def get_percentage(self):
|
|
"""Get the current progress percentage. Assumes 100% if max_value is 0
|
|
"""
|
|
if self.get_max_value() == 0:
|
|
return 100.0
|
|
else:
|
|
return self.get_current_value() * 100.0 / self.get_max_value()
|
|
|
|
def get_current_value(self):
|
|
return self._current_value
|
|
|
|
def get_max_value(self):
|
|
return self._max_value
|
|
|
|
def _set_max_value(self, max_value):
|
|
self._max_value = max_value
|
|
|
|
def _need_update(self):
|
|
cur_val = self.get_current_value()
|
|
if cur_val < 100:
|
|
return cur_val - self.last_update > 10
|
|
elif cur_val < 500:
|
|
return cur_val - self.last_update > 50
|
|
else:
|
|
return cur_val - self.last_update > 100
|
|
|
|
|
|
class MultiplexingObserver(Observer):
|
|
"""Combine multiple observers into one.
|
|
"""
|
|
def __init__(self, *components):
|
|
self.components = components
|
|
super(MultiplexingObserver, self).__init__()
|
|
|
|
def start(self, max_value):
|
|
for o in self.components:
|
|
o.start(max_value)
|
|
super(MultiplexingObserver, self).start(max_value)
|
|
|
|
def finish(self):
|
|
for o in self.components:
|
|
o.finish()
|
|
super(MultiplexingObserver, self).finish()
|
|
|
|
def update(self, current_value):
|
|
for o in self.components:
|
|
o.update(current_value)
|
|
super(MultiplexingObserver, self).update(current_value)
|
|
|
|
|
|
class ServerAnnounceObserver(Observer):
|
|
"""Send the output to a Minecraft server via FIFO or stdin"""
|
|
def __init__(self, target='/dev/null', pct_interval=10):
|
|
self.pct_interval = pct_interval
|
|
self.target_handle = open(target, 'w')
|
|
self.last_update = 0
|
|
super(ServerAnnounceObserver, self).__init__()
|
|
|
|
def start(self, max_value):
|
|
self._send_output('Starting render of %d total tiles' % max_value)
|
|
super(ServerAnnounceObserver, self).start(max_value)
|
|
|
|
def finish(self):
|
|
self._send_output('Render complete!')
|
|
super(ServerAnnounceObserver, self).finish()
|
|
self.target_handle.close()
|
|
|
|
def update(self, current_value):
|
|
super(ServerAnnounceObserver, self).update(current_value)
|
|
if self._need_update():
|
|
self._send_output('Rendered %d of %d tiles, %d%% complete' %
|
|
(self.get_current_value(), self.get_max_value(),
|
|
self.get_percentage()))
|
|
self.last_update = current_value
|
|
|
|
def _need_update(self):
|
|
return(self.get_percentage() -
|
|
(self.last_update * 100.0 / self.get_max_value())
|
|
>= self.pct_interval)
|
|
|
|
def _send_output(self, output):
|
|
self.target_handle.write('say %s\n' % output)
|
|
self.target_handle.flush()
|
|
|
|
|
|
# Fair amount of code duplication incoming
|
|
# Perhaps both ServerAnnounceObserver and RConObserver
|
|
# could share the percentage interval code.
|
|
|
|
class RConObserver(Observer):
|
|
"""Send the output to a Minecraft server via rcon"""
|
|
|
|
def __init__(self, target, password, port=25575, pct_interval=10):
|
|
self.pct_interval = pct_interval
|
|
self.conn = rcon.RConConnection(target, port)
|
|
self.conn.login(password)
|
|
self.last_update = 0
|
|
super(RConObserver, self).__init__()
|
|
|
|
def start(self, max_value):
|
|
self._send_output("Starting render of %d total tiles" % max_value)
|
|
super(RConObserver, self).start(max_value)
|
|
|
|
def finish(self):
|
|
self._send_output("Render complete!")
|
|
super(RConObserver, self).finish()
|
|
self.conn.close()
|
|
|
|
def update(self, current_value):
|
|
super(RConObserver, self).update(current_value)
|
|
if self._need_update():
|
|
self._send_output('Rendered %d of %d tiles, %d%% complete' %
|
|
(self.get_current_value(), self.get_max_value(),
|
|
self.get_percentage()))
|
|
self.last_update = current_value
|
|
|
|
def _need_update(self):
|
|
if self.get_max_value() > 0:
|
|
return(self.get_percentage() - (self.last_update * 100.0 / self.get_max_value())
|
|
>= self.pct_interval)
|
|
else:
|
|
return True
|
|
|
|
def _send_output(self, output):
|
|
self.conn.command("say", output)
|