forked from platformio/platformio-core
		
	
		
			
				
	
	
		
			267 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			267 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Copyright (C) Ivan Kravets <me@ikravets.com>
 | 
						|
# 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())
 | 
						|
 | 
						|
    def _prefill_screen_name(self):
 | 
						|
        args = [str(s).lower() for s in sys_argv[1:]]
 | 
						|
        if not args:
 | 
						|
            return
 | 
						|
 | 
						|
        if args[0] in ("lib", "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
 |