# Copyright (C) Ivan Kravets # See LICENSE for details. import atexit import platform import re import threading import uuid from sys import argv as sys_argv from time import time import click import requests from platformio import __version__, app from platformio.util import exec_command, get_systype class TelemetryBase(object): MACHINE_ID = str(uuid.uuid5(uuid.NAMESPACE_OID, str(uuid.getnode()))) def __init__(self): self._params = {} def __getitem__(self, name): return self._params.get(name, None) def __setitem__(self, name, value): self._params[name] = value def __delitem__(self, name): if name in self._params: del self._params[name] def send(self, hittype): raise NotImplementedError() class MeasurementProtocol(TelemetryBase): TRACKING_ID = "UA-1768265-9" PARAMS_MAP = { "screen_name": "cd", "event_category": "ec", "event_action": "ea", "event_label": "el", "event_value": "ev" } def __init__(self): TelemetryBase.__init__(self) self['v'] = 1 self['tid'] = self.TRACKING_ID self['cid'] = self.MACHINE_ID self['sr'] = "%dx%d" % click.get_terminal_size() self._prefill_screen_name() self._prefill_appinfo() self._prefill_custom_data() def __getitem__(self, name): if name in self.PARAMS_MAP: name = self.PARAMS_MAP[name] return TelemetryBase.__getitem__(self, name) def __setitem__(self, name, value): if name in self.PARAMS_MAP: name = self.PARAMS_MAP[name] TelemetryBase.__setitem__(self, name, value) def _prefill_appinfo(self): self['av'] = __version__ # gather dependent packages dpdata = [] dpdata.append("Click/%s" % click.__version__) # dpdata.append("Requests/%s" % requests.__version__) try: result = exec_command(["scons", "--version"]) match = re.search(r"engine: v([\d\.]+)", result['out']) if match: dpdata.append("SCons/%s" % match.group(1)) except: # pylint: disable=W0702 pass self['an'] = " ".join(dpdata) def _prefill_custom_data(self): self['cd1'] = get_systype() self['cd2'] = "Python/%s %s" % (platform.python_version(), platform.platform()) self['cd4'] = 1 if app.get_setting("enable_prompts") else 0 def _prefill_screen_name(self): args = [str(s).lower() for s in sys_argv[1:]] if not args: return if args[0] in ("lib", "platforms", "serialports", "settings"): cmd_path = args[:2] else: cmd_path = args[:1] self['screen_name'] = " ".join([p.title() for p in cmd_path]) self['cd3'] = " ".join(args) def send(self, hittype): if not app.get_setting("enable_telemetry"): return self['t'] = hittype # correct queue time if "qt" in self._params and isinstance(self['qt'], float): self['qt'] = int((time() - self['qt']) * 1000) MPDataPusher.get_instance().push(self._params) class MPDataPusher(threading.Thread): @classmethod def get_instance(cls): try: return cls._thinstance except AttributeError: cls._event = threading.Event() cls._thinstance = cls() cls._thinstance.start() return cls._thinstance @classmethod def http_session(cls): try: return cls._http_session except AttributeError: cls._http_session = requests.Session() return cls._http_session def __init__(self): threading.Thread.__init__(self) self._terminate = False self._server_online = False self._stack = [] def run(self): while not self._terminate: self._event.wait() if self._terminate or not self._stack: return self._event.clear() data = self._stack.pop() try: r = self.http_session().post( "https://ssl.google-analytics.com/collect", data=data, timeout=3 ) r.raise_for_status() self._server_online = True except: # pylint: disable=W0702 self._server_online = False self._stack.append(data) def push(self, data): self._stack.append(data) self._event.set() def is_server_online(self): return self._server_online def get_stack_data(self): return self._stack def join(self, timeout=0.1): self._terminate = True self._event.set() self.http_session().close() threading.Thread.join(self, timeout) @atexit.register def _finalize(): MAX_RESEND_REPORTS = 10 mpdp = MPDataPusher.get_instance() backup_reports(mpdp.get_stack_data()) resent_nums = 0 while mpdp.is_server_online() and resent_nums < MAX_RESEND_REPORTS: if not resend_backuped_report(): break resent_nums += 1 def on_command(ctx): # pylint: disable=W0613 mp = MeasurementProtocol() mp.send("screenview") def on_run_environment(options, targets): opts = ["%s=%s" % (opt, value) for opt, value in sorted(options.items())] targets = [t.title() for t in targets or ["run"]] on_event("Env", " ".join(targets), "&".join(opts)) def on_event(category, action, label=None, value=None, screen_name=None): mp = MeasurementProtocol() mp['event_category'] = category[:150] mp['event_action'] = action[:500] if label: mp['event_label'] = label[:500] if value: mp['event_value'] = int(value) if screen_name: mp['screen_name'] = screen_name[:2048] mp.send("event") def on_exception(e): mp = MeasurementProtocol() mp['exd'] = "%s: %s" % (type(e).__name__, e) mp['exf'] = 1 mp.send("exception") def backup_reports(data): if not data: return KEEP_MAX_REPORTS = 1000 tm = app.get_state_item("telemetry", {}) if "backup" not in tm: tm['backup'] = [] for params in data: # skip static options for key in params.keys(): if key in ("v", "tid", "cid", "cd1", "cd2", "sr", "an"): del params[key] # store time in UNIX format if "qt" not in params: params['qt'] = time() elif not isinstance(params['qt'], float): params['qt'] = time() - (params['qt'] / 1000) tm['backup'].append(params) tm['backup'] = tm['backup'][KEEP_MAX_REPORTS*-1:] app.set_state_item("telemetry", tm) def resend_backuped_report(): tm = app.get_state_item("telemetry", {}) if "backup" not in tm or not tm['backup']: return False report = tm['backup'].pop() app.set_state_item("telemetry", tm) mp = MeasurementProtocol() for key, value in report.items(): mp[key] = value mp.send(report['t']) return True