0
This repository has been archived on 2025-04-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Minecraft-Overviewer/overviewer_core/observer.py
Nicolas F 3d31347e9b observer: create outputdir if it doesn't exist
Observers get initialised at config parse time, before Overviewer
creates the outputdir. So we can just tell them to create the
outputdir if it doesn't already exist to avoid an awkward error
on the initial render.

Fixes #1657.
2019-10-27 20:19:07 +01:00

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)