from path import Path
import platform
import time
from logging import getLogger
from circuits import Component, Timer, Worker, task
from .circus.commands import Status as CircusStatus
from .events import (adjust_systime, command, handle,
download_disabled,
get_program, get_info,
heartbeat, initial_data_ready,
new_program, new_info,
supervisor_status,
time_adjusted, )
from .remote.client import http_get, http_post, Endpoint
from .utils import tz, now, tnow, get_time_offset, add_time_offset, set_timezone, datetime_format
from .utils.hwinfo import HWInfo
from .utils.stream import MediaStream
from .version import version
from .settings import HEARTBEAT_INTERVAL, TIME_DIFF_LIMIT, SYSTIME_DIFF_LIMIT, VERSION_PREFIX
from .storage import DataNotPresent, InitialProgramData, ProgramData, PlayerInfoData, NewProgramData
CMD_EVENTS = {
'pr': get_program,
'in': get_info
}
[docs]class WebClient(Component):
"""
Sends heartbeat to server with status data, can get a command in response.
Command will be sent to workshop component, web commands will be executed
immediately - getting player info or program data.
Another task is logging played songs to server.
"""
NUM_WORKERS = 3
def init(self, config):
self.logger = getLogger(__name__)
self.logger.debug('init: %r', self)
self.config = config
self.data_dir = Path(config['data_dir']).expanduser()
self.token = config['token']
self.remote_url = config['remote_url']
self.connection = {
'token': self.token,
'remote_url': self.remote_url
}
self.worker = Worker(workers=self.NUM_WORKERS, channel='webclient').register(self)
self.programs_storage = ProgramData(self.data_dir)
self.info_storage = PlayerInfoData(self.data_dir)
self.confirmed = None
self.status = {}
self.initial = True
self.logger.info('Player: %s, version: %s', platform.node(), self._version())
def started(self, component, *args):
if component.name == 'App':
self.logger.info('started: %r from %r', self, component)
Timer(HEARTBEAT_INTERVAL, heartbeat(), persist=True).register(self)
Timer(HEARTBEAT_INTERVAL-2, supervisor_status(), persist=True).register(self)
self.fire(heartbeat())
def _version(self):
# canonical version format
v = '.'.join(version.split('.')[:3])
return '{}-{}'.format(VERSION_PREFIX, v)
def _get_data(self):
try:
program_hash = self.programs_storage.get_hash()
summary = MediaStream(self.programs_storage).summary()
except DataNotPresent:
program_hash = ''
summary = {}
summary['system_time'] = time.time()
summary['time_offset'] = get_time_offset()
summary['tzname'] = tz.zone
data = {
'name': platform.node(),
'version': self._version(),
'timestamp': tnow(),
#'system_time': time.time(),
#'time_offset': get_time_offset(),
#'tzname': tz.zone,
'status': self.status.copy(),
'program_hash': program_hash,
'info_hash': self.info_storage.get_hash(),
'techinfo': HWInfo(self.data_dir).collect()
}
if summary:
data['summary'] = summary
# set default to send next time if not updated
self.status['playing'] = 'stopped'
if self.confirmed:
data['command'] = self.confirmed
self.confirmed = None
self.logger.info('\tconfirm command: %s', data['command'])
return data
def _dispatch_command(self, cmd):
if cmd in CMD_EVENTS:
event = CMD_EVENTS[cmd]
self.fire(event())
else:
if self.parent:
self.fire(command(cmd))
def _handle_time_offset(self, server_timestamp, original_timestamp):
if original_timestamp is not None:
offset = server_timestamp - (tnow() + original_timestamp) / 2
if self.initial and abs(offset) > SYSTIME_DIFF_LIMIT:
self.logger.info('Server time difference: %.2fs', offset)
self.fire(adjust_systime(offset))
elif abs(offset) > TIME_DIFF_LIMIT:
self.logger.info('Adjusting application time offset to: %.2fs', offset)
add_time_offset(offset)
self.fire(time_adjusted())
[docs] def set_status(self, status):
""" used states:
playing,
stopped,
not_allowed,
no_song
"""
self.status['playing'] = status
[docs] def set_dwld_status(self, status):
""" used states:
download,
waiting,
suspended,
finished
"""
self.status['download'] = status
def supervisor_status(self):
self.status['supervisor'] = CircusStatus().run()
[docs] def task_success(self, event, task, response, **kw):
""" returned from succesful task """
if 'webclient' in event.channels:
endpoint = task.args[1]
success, data = response
if success:
handle = getattr(self, 'handle_' + str(endpoint), None)
if handle:
handle(data, **kw)
else:
self.logger.warning('Task %s failed: %s', endpoint, data)
[docs] def task_failure(self, event, task, error, **kw):
""" returned from failed task """
if 'webclient' in event.channels:
self.logger.error(error[1])
def heartbeat(self):
self.logger.info('heartbeat start')
data = self._get_data()
self.fire(task(http_post,
Endpoint.PlayerCheck,
data=data,
**self.connection), 'webclient')
def confirm_cmd(self, cmd):
self.logger.info('confirm command: %s', cmd)
self.confirmed = cmd
# send the confirmation immediately
self.heartbeat()
[docs] def update_finished(self, exit_code):
""" send update exit code to server """
self.status['update'] = exit_code
self.heartbeat()
[docs] def handle_check(self, response):
""" process data from server """
if response:
if response.get('status', None) == 'OK':
dd = response.get('download_disabled', None)
# can be true or false
if dd is not None:
self.fire(download_disabled(dd))
if response.get('command', None):
cmd = response['command']
self.logger.info('heartbeat command: %s', cmd)
self._dispatch_command(cmd)
if self.initial and response.get('command', None) != 'pr':
self.fire(get_program(initial=True))
server_timestamp = response.get('server_timestamp', None)
if server_timestamp:
original_timestamp = response.get('player_timestamp', None)
self._handle_time_offset(server_timestamp, original_timestamp)
tz = response.get('time_zone', None)
if tz:
set_timezone(tz)
self.fire(time_adjusted())
else:
self.logger.warning('Strange server response: %s', response)
else:
self.logger.warning('No server response: %s', response)
def get_program(self, initial=False):
# get program data from remote server
self.logger.info('get program')
data = {'initial': True} if initial else None
self.fire(task(http_post,
Endpoint.ProgramData,
data=data,
**self.connection), 'webclient')
def handle_data(self, data):
self.logger.info('handling program data')
if data:
# save initial data if present
initial = data.get('initial', None)
if initial is not None:
inst = InitialProgramData(self.data_dir)
inst.save(initial)
self.logger.info('Saved initial program data')
# save program data if changed
changed = False
programs = data.get('programs', None)
if programs is not None:
storage = ProgramData(self.data_dir)
if storage.exists():
newstorage = NewProgramData(self.data_dir)
changed = newstorage.save(programs)
else:
storage.save(programs)
changed = True
if changed:
self.fire(new_program())
else:
self.logger.debug('program not changed')
else:
self.logger.warning('No program data from server')
self.initial = False
def get_info(self):
self.logger.info('get info')
self.fire(task(http_get,
Endpoint.PlayerInfo,
**self.connection), 'webclient')
def handle_info(self, data):
self.logger.info('handling player info')
if data:
info = data.get('info', None)
# save
if info:
changed = self.info_storage.save(info)
if changed:
self.fire(new_info())
else:
self.logger.info('player info not changed')
def log_song(self, song):
self.logger.info('Log song: %s', song)
data = {
'song': song.filename,
'item_id': song.item.id,
'time': datetime_format(now())
}
self.fire(task(http_post,
Endpoint.LogSong,
data=data,
**self.connection), 'webclient')
def warning(self, warning, msg):
self.logger.info('Report warning: %s', msg)
data = dict(warning=warning, description=msg)
self.fire(task(http_post,
Endpoint.Warning,
data=data,
**self.connection), 'webclient')
def screenshot(self, path, timestamp):
self.logger.info('Sending screeenshot: %s', path)
files = {'screenshot': open(path,'rb')}
data = {'timestamp': timestamp}
self.fire(task(http_post,
Endpoint.Screenshot,
data=data,
files=files,
**self.connection), 'webclient')
def lastlogs(self, path, timestamp):
self.logger.info('Sending logs: %s', path)
files = {'lastlogs': open(path,'rb')}
data = {'timestamp': timestamp}
self.fire(task(http_post,
Endpoint.SendLogs,
data=data,
files=files,
**self.connection), 'webclient')