mirror of
https://github.com/platformio/platformio-core.git
synced 2025-07-30 10:07:14 +02:00
Allow multiple instances of @PlatformIO
This commit is contained in:
@ -1,3 +1,3 @@
|
|||||||
[settings]
|
[settings]
|
||||||
line_length=79
|
line_length=79
|
||||||
known_third_party=click,requests,serial,SCons,pytest,bottle
|
known_third_party=bottle,click,lockfile,pytest,requests,serial,SCons
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Copyright (C) Ivan Kravets <me@ikravets.com>
|
# Copyright (C) Ivan Kravets <me@ikravets.com>
|
||||||
# See LICENSE for details.
|
# See LICENSE for details.
|
||||||
|
|
||||||
VERSION = (2, 3, "0a3")
|
VERSION = (2, 3, "0b1")
|
||||||
__version__ = ".".join([str(s) for s in VERSION])
|
__version__ = ".".join([str(s) for s in VERSION])
|
||||||
|
|
||||||
__title__ = "platformio"
|
__title__ = "platformio"
|
||||||
|
@ -55,15 +55,16 @@ class PlatformioCLI(click.MultiCommand): # pylint: disable=R0904
|
|||||||
context_settings=dict(help_option_names=["-h", "--help"]))
|
context_settings=dict(help_option_names=["-h", "--help"]))
|
||||||
@click.version_option(__version__, prog_name="PlatformIO")
|
@click.version_option(__version__, prog_name="PlatformIO")
|
||||||
@click.option("--force", "-f", is_flag=True,
|
@click.option("--force", "-f", is_flag=True,
|
||||||
help="Force to accept any confirmation prompts")
|
help="Force to accept any confirmation prompts.")
|
||||||
|
@click.option("--caller", "-c", help="Caller ID (service).")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, force):
|
def cli(ctx, force, caller):
|
||||||
maintenance.on_platformio_start(ctx, force)
|
maintenance.on_platformio_start(ctx, force, caller)
|
||||||
|
|
||||||
|
|
||||||
@cli.resultcallback()
|
@cli.resultcallback()
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def process_result(ctx, result, force): # pylint: disable=W0613
|
def process_result(ctx, result, force, caller): # pylint: disable=W0613
|
||||||
maintenance.on_platformio_end(ctx, result)
|
maintenance.on_platformio_end(ctx, result)
|
||||||
|
|
||||||
|
|
||||||
@ -81,7 +82,7 @@ def main():
|
|||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
cli(None, None)
|
cli(None, None, None)
|
||||||
except Exception as e: # pylint: disable=W0703
|
except Exception as e: # pylint: disable=W0703
|
||||||
if not isinstance(e, exception.ReturnErrorCode):
|
if not isinstance(e, exception.ReturnErrorCode):
|
||||||
maintenance.on_platformio_exception(e)
|
maintenance.on_platformio_exception(e)
|
||||||
|
@ -5,6 +5,8 @@ import json
|
|||||||
from os import environ, getenv
|
from os import environ, getenv
|
||||||
from os.path import isfile, join
|
from os.path import isfile, join
|
||||||
|
|
||||||
|
from lockfile import LockFile
|
||||||
|
|
||||||
from platformio import __version__
|
from platformio import __version__
|
||||||
from platformio.exception import InvalidSettingName, InvalidSettingValue
|
from platformio.exception import InvalidSettingName, InvalidSettingValue
|
||||||
from platformio.util import get_home_dir, is_ci
|
from platformio.util import get_home_dir, is_ci
|
||||||
@ -48,7 +50,9 @@ DEFAULT_SETTINGS = {
|
|||||||
|
|
||||||
|
|
||||||
SESSION_VARS = {
|
SESSION_VARS = {
|
||||||
"force_option": False
|
"command_ctx": None,
|
||||||
|
"force_option": False,
|
||||||
|
"caller_id": None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -59,22 +63,30 @@ class State(object):
|
|||||||
if not self.path:
|
if not self.path:
|
||||||
self.path = join(get_home_dir(), "appstate.json")
|
self.path = join(get_home_dir(), "appstate.json")
|
||||||
self._state = {}
|
self._state = {}
|
||||||
|
self._prev_state = {}
|
||||||
|
self._lock = None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
try:
|
try:
|
||||||
if isfile(self.path):
|
if isfile(self.path):
|
||||||
|
self._lock = LockFile(self.path)
|
||||||
|
self._lock.acquire()
|
||||||
with open(self.path, "r") as fp:
|
with open(self.path, "r") as fp:
|
||||||
self._state = json.load(fp)
|
self._state = json.load(fp)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self._state = {}
|
self._state = {}
|
||||||
|
self._prev_state = self._state.copy()
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
def __exit__(self, type_, value, traceback):
|
def __exit__(self, type_, value, traceback):
|
||||||
with open(self.path, "w") as fp:
|
if self._prev_state != self._state:
|
||||||
if "dev" in __version__:
|
with open(self.path, "w") as fp:
|
||||||
json.dump(self._state, fp, indent=4)
|
if "dev" in __version__:
|
||||||
else:
|
json.dump(self._state, fp, indent=4)
|
||||||
json.dump(self._state, fp)
|
else:
|
||||||
|
json.dump(self._state, fp)
|
||||||
|
if self._lock:
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
|
|
||||||
def sanitize_setting(name, value):
|
def sanitize_setting(name, value):
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import struct
|
import struct
|
||||||
import sys
|
|
||||||
from os import remove
|
from os import remove
|
||||||
from os.path import isdir, isfile, join
|
from os.path import isdir, isfile, join
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
@ -23,14 +22,14 @@ from platformio.platforms.base import PlatformFactory
|
|||||||
from platformio.util import get_home_dir
|
from platformio.util import get_home_dir
|
||||||
|
|
||||||
|
|
||||||
def on_platformio_start(ctx, force):
|
def on_platformio_start(ctx, force, caller):
|
||||||
|
app.set_session_var("command_ctx", ctx)
|
||||||
app.set_session_var("force_option", force)
|
app.set_session_var("force_option", force)
|
||||||
telemetry.on_command(ctx)
|
app.set_session_var("caller_id", caller)
|
||||||
|
telemetry.on_command()
|
||||||
|
|
||||||
# skip any check operations when upgrade process
|
# skip any check operations when upgrade command
|
||||||
args = [str(s).lower() for s in sys.argv[1:]
|
if len(ctx.args or []) and ctx.args[0] == "upgrade":
|
||||||
if not str(s).startswith("-")]
|
|
||||||
if len(args) > 1 and args[1] == "upgrade":
|
|
||||||
return
|
return
|
||||||
|
|
||||||
after_upgrade(ctx)
|
after_upgrade(ctx)
|
||||||
|
@ -3,17 +3,20 @@
|
|||||||
|
|
||||||
import atexit
|
import atexit
|
||||||
import platform
|
import platform
|
||||||
|
import Queue
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections import deque
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from time import time
|
from time import sleep, time
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from platformio import __version__, app, exception, util
|
from platformio import __version__, app, exception, util
|
||||||
|
from platformio.ide.projectgenerator import ProjectGenerator
|
||||||
|
|
||||||
|
|
||||||
class TelemetryBase(object):
|
class TelemetryBase(object):
|
||||||
@ -75,7 +78,8 @@ class MeasurementProtocol(TelemetryBase):
|
|||||||
# gather dependent packages
|
# gather dependent packages
|
||||||
dpdata = []
|
dpdata = []
|
||||||
dpdata.append("Click/%s" % click.__version__)
|
dpdata.append("Click/%s" % click.__version__)
|
||||||
# dpdata.append("Requests/%s" % requests.__version__)
|
if app.get_session_var("caller_id"):
|
||||||
|
dpdata.append("Caller/%s" % app.get_session_var("caller_id"))
|
||||||
try:
|
try:
|
||||||
result = util.exec_command(["scons", "--version"])
|
result = util.exec_command(["scons", "--version"])
|
||||||
match = re.search(r"engine: v([\d\.]+)", result['out'])
|
match = re.search(r"engine: v([\d\.]+)", result['out'])
|
||||||
@ -89,21 +93,21 @@ class MeasurementProtocol(TelemetryBase):
|
|||||||
self['cd1'] = util.get_systype()
|
self['cd1'] = util.get_systype()
|
||||||
self['cd2'] = "Python/%s %s" % (platform.python_version(),
|
self['cd2'] = "Python/%s %s" % (platform.python_version(),
|
||||||
platform.platform())
|
platform.platform())
|
||||||
self['cd4'] = 1 if app.get_setting("enable_prompts") else 0
|
self['cd4'] = (1 if app.get_setting("enable_prompts") and
|
||||||
|
not app.get_session_var("caller_id") else 0)
|
||||||
|
|
||||||
def _prefill_screen_name(self):
|
def _prefill_screen_name(self):
|
||||||
args = [str(s).lower() for s in sys.argv[1:]
|
self['cd3'] = " ".join([str(s).lower() for s in sys.argv[1:]])
|
||||||
if not str(s).startswith("-")]
|
|
||||||
|
ctx_args = app.get_session_var("command_ctx").args or []
|
||||||
|
args = [str(s).lower() for s in ctx_args if not str(s).startswith("-")]
|
||||||
if not args:
|
if not args:
|
||||||
return
|
return
|
||||||
|
|
||||||
if args[0] in ("lib", "platforms", "serialports", "settings"):
|
if args[0] in ("lib", "platforms", "serialports", "settings"):
|
||||||
cmd_path = args[:2]
|
cmd_path = args[:2]
|
||||||
else:
|
else:
|
||||||
cmd_path = args[:1]
|
cmd_path = args[:1]
|
||||||
|
|
||||||
self['screen_name'] = " ".join([p.title() for p in cmd_path])
|
self['screen_name'] = " ".join([p.title() for p in cmd_path])
|
||||||
self['cd3'] = " ".join([str(s).lower() for s in sys.argv[1:]])
|
|
||||||
|
|
||||||
def send(self, hittype):
|
def send(self, hittype):
|
||||||
if not app.get_setting("enable_telemetry"):
|
if not app.get_setting("enable_telemetry"):
|
||||||
@ -115,92 +119,101 @@ class MeasurementProtocol(TelemetryBase):
|
|||||||
if "qt" in self._params and isinstance(self['qt'], float):
|
if "qt" in self._params and isinstance(self['qt'], float):
|
||||||
self['qt'] = int((time() - self['qt']) * 1000)
|
self['qt'] = int((time() - self['qt']) * 1000)
|
||||||
|
|
||||||
MPDataPusher.get_instance().push(self._params)
|
MPDataPusher().push(self._params)
|
||||||
|
|
||||||
|
|
||||||
class MPDataPusher(threading.Thread):
|
@util.singleton
|
||||||
|
class MPDataPusher(object):
|
||||||
|
|
||||||
@classmethod
|
MAX_WORKERS = 5
|
||||||
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):
|
def __init__(self):
|
||||||
threading.Thread.__init__(self)
|
self._queue = Queue.LifoQueue()
|
||||||
self._terminate = False
|
self._failedque = deque()
|
||||||
self._server_online = False
|
self._http_session = requests.Session()
|
||||||
self._stack = []
|
self._http_offline = False
|
||||||
|
self._workers = []
|
||||||
|
|
||||||
def run(self):
|
def push(self, item):
|
||||||
while not self._terminate:
|
# if network is off-line
|
||||||
self._event.wait()
|
if self._http_offline:
|
||||||
if self._terminate or not self._stack:
|
if "qt" not in item:
|
||||||
return
|
item['qt'] = time()
|
||||||
self._event.clear()
|
self._failedque.append(item)
|
||||||
|
return
|
||||||
|
|
||||||
data = self._stack.pop()
|
self._queue.put(item)
|
||||||
try:
|
self._tune_workers()
|
||||||
r = self.http_session().post(
|
|
||||||
"https://ssl.google-analytics.com/collect",
|
|
||||||
data=data,
|
|
||||||
headers=util.get_request_defheaders(),
|
|
||||||
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):
|
def in_wait(self):
|
||||||
self._stack.append(data)
|
return self._queue.unfinished_tasks
|
||||||
self._event.set()
|
|
||||||
|
|
||||||
def is_server_online(self):
|
def get_items(self):
|
||||||
return self._server_online
|
items = list(self._failedque)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
items.append(self._queue.get_nowait())
|
||||||
|
except Queue.Empty:
|
||||||
|
pass
|
||||||
|
return items
|
||||||
|
|
||||||
def get_stack_data(self):
|
def _tune_workers(self):
|
||||||
return self._stack
|
for i, w in enumerate(self._workers):
|
||||||
|
if not w.is_alive():
|
||||||
|
del self._workers[i]
|
||||||
|
|
||||||
def join(self, timeout=0.1):
|
need_nums = min(self._queue.qsize(), self.MAX_WORKERS)
|
||||||
self._terminate = True
|
active_nums = len(self._workers)
|
||||||
self._event.set()
|
if need_nums <= active_nums:
|
||||||
self.http_session().close()
|
return
|
||||||
threading.Thread.join(self, timeout)
|
|
||||||
|
for i in range(need_nums - active_nums):
|
||||||
|
t = threading.Thread(target=self._worker)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
self._workers.append(t)
|
||||||
|
|
||||||
|
def _worker(self):
|
||||||
|
while True:
|
||||||
|
item = self._queue.get()
|
||||||
|
_item = item.copy()
|
||||||
|
if "qt" not in _item:
|
||||||
|
_item['qt'] = time()
|
||||||
|
self._failedque.append(_item)
|
||||||
|
if self._send_data(item):
|
||||||
|
self._failedque.remove(_item)
|
||||||
|
else:
|
||||||
|
self._http_offline = True
|
||||||
|
self._queue.task_done()
|
||||||
|
|
||||||
|
def _send_data(self, data):
|
||||||
|
result = False
|
||||||
|
try:
|
||||||
|
r = self._http_session.post(
|
||||||
|
"https://ssl.google-analytics.com/collect",
|
||||||
|
data=data,
|
||||||
|
headers=util.get_request_defheaders(),
|
||||||
|
timeout=2
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
result = True
|
||||||
|
except: # pylint: disable=W0702
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@atexit.register
|
def on_command():
|
||||||
def _finalize():
|
resend_backuped_reports()
|
||||||
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 = MeasurementProtocol()
|
||||||
mp.send("screenview")
|
mp.send("screenview")
|
||||||
|
|
||||||
if util.is_ci():
|
if util.is_ci():
|
||||||
measure_ci()
|
measure_ci()
|
||||||
|
|
||||||
|
if app.get_session_var("calller_id"):
|
||||||
|
measure_caller(app.get_session_var("calller_id"))
|
||||||
|
|
||||||
|
|
||||||
def measure_ci():
|
def measure_ci():
|
||||||
event = {
|
event = {
|
||||||
@ -226,6 +239,18 @@ def measure_ci():
|
|||||||
on_event(**event)
|
on_event(**event)
|
||||||
|
|
||||||
|
|
||||||
|
def measure_caller(calller_id):
|
||||||
|
calller_id = str(calller_id)[:20].lower()
|
||||||
|
event = {
|
||||||
|
"category": "Caller",
|
||||||
|
"action": "Misc",
|
||||||
|
"label": calller_id
|
||||||
|
}
|
||||||
|
if calller_id in ProjectGenerator.get_supported_ides():
|
||||||
|
event['action'] = "IDE"
|
||||||
|
on_event(**event)
|
||||||
|
|
||||||
|
|
||||||
def on_run_environment(options, targets):
|
def on_run_environment(options, targets):
|
||||||
opts = ["%s=%s" % (opt, value) for opt, value in sorted(options.items())]
|
opts = ["%s=%s" % (opt, value) for opt, value in sorted(options.items())]
|
||||||
targets = [t.title() for t in targets or ["run"]]
|
targets = [t.title() for t in targets or ["run"]]
|
||||||
@ -254,16 +279,28 @@ def on_exception(e):
|
|||||||
mp.send("exception")
|
mp.send("exception")
|
||||||
|
|
||||||
|
|
||||||
def backup_reports(data):
|
@atexit.register
|
||||||
if not data:
|
def _finalize():
|
||||||
|
timeout = 1000 # msec
|
||||||
|
elapsed = 0
|
||||||
|
while elapsed < timeout:
|
||||||
|
if not MPDataPusher().in_wait():
|
||||||
|
break
|
||||||
|
sleep(0.2)
|
||||||
|
elapsed += 200
|
||||||
|
backup_reports(MPDataPusher().get_items())
|
||||||
|
|
||||||
|
|
||||||
|
def backup_reports(items):
|
||||||
|
if not items:
|
||||||
return
|
return
|
||||||
|
|
||||||
KEEP_MAX_REPORTS = 1000
|
KEEP_MAX_REPORTS = 100
|
||||||
tm = app.get_state_item("telemetry", {})
|
tm = app.get_state_item("telemetry", {})
|
||||||
if "backup" not in tm:
|
if "backup" not in tm:
|
||||||
tm['backup'] = []
|
tm['backup'] = []
|
||||||
|
|
||||||
for params in data:
|
for params in items:
|
||||||
# skip static options
|
# skip static options
|
||||||
for key in params.keys():
|
for key in params.keys():
|
||||||
if key in ("v", "tid", "cid", "cd1", "cd2", "sr", "an"):
|
if key in ("v", "tid", "cid", "cd1", "cd2", "sr", "an"):
|
||||||
@ -277,21 +314,21 @@ def backup_reports(data):
|
|||||||
|
|
||||||
tm['backup'].append(params)
|
tm['backup'].append(params)
|
||||||
|
|
||||||
tm['backup'] = tm['backup'][KEEP_MAX_REPORTS*-1:]
|
tm['backup'] = tm['backup'][KEEP_MAX_REPORTS * -1:]
|
||||||
app.set_state_item("telemetry", tm)
|
app.set_state_item("telemetry", tm)
|
||||||
|
|
||||||
|
|
||||||
def resend_backuped_report():
|
def resend_backuped_reports():
|
||||||
tm = app.get_state_item("telemetry", {})
|
tm = app.get_state_item("telemetry", {})
|
||||||
if "backup" not in tm or not tm['backup']:
|
if "backup" not in tm or not tm['backup']:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
report = tm['backup'].pop()
|
for report in tm['backup']:
|
||||||
|
mp = MeasurementProtocol()
|
||||||
|
for key, value in report.items():
|
||||||
|
mp[key] = value
|
||||||
|
mp.send(report['t'])
|
||||||
|
|
||||||
|
# clean
|
||||||
|
tm['backup'] = []
|
||||||
app.set_state_item("telemetry", tm)
|
app.set_state_item("telemetry", tm)
|
||||||
|
|
||||||
mp = MeasurementProtocol()
|
|
||||||
for key, value in report.items():
|
|
||||||
mp[key] = value
|
|
||||||
mp.send(report['t'])
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
@ -100,6 +100,17 @@ class memoized(object):
|
|||||||
return functools.partial(self.__call__, obj)
|
return functools.partial(self.__call__, obj)
|
||||||
|
|
||||||
|
|
||||||
|
def singleton(cls):
|
||||||
|
""" From PEP-318 http://www.python.org/dev/peps/pep-0318/#examples """
|
||||||
|
_instances = {}
|
||||||
|
|
||||||
|
def get_instance(*args, **kwargs):
|
||||||
|
if cls not in _instances:
|
||||||
|
_instances[cls] = cls(*args, **kwargs)
|
||||||
|
return _instances[cls]
|
||||||
|
return get_instance
|
||||||
|
|
||||||
|
|
||||||
def get_systype():
|
def get_systype():
|
||||||
data = uname()
|
data = uname()
|
||||||
systype = data[0]
|
systype = data[0]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
bottle==0.12.8
|
bottle==0.12.8
|
||||||
click==5.0
|
click==5.1
|
||||||
colorama==0.3.3
|
colorama==0.3.3
|
||||||
|
lockfile==0.10.2
|
||||||
pyserial==2.7
|
pyserial==2.7
|
||||||
requests==2.7.0
|
requests==2.7.0
|
||||||
scons==2.3.6
|
scons==2.3.6
|
||||||
|
Reference in New Issue
Block a user