sublime-wakatime/packages/wakatime/main.py

395 lines
13 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""
2015-09-29 13:11:25 +03:00
wakatime.main
2015-03-10 01:23:29 +03:00
~~~~~~~~~~~~~
2017-02-21 03:18:38 +03:00
Module entry point.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
from __future__ import print_function
import base64
import logging
import os
import re
import sys
import traceback
import socket
2016-03-06 23:48:42 +03:00
pwd = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(pwd))
sys.path.insert(0, os.path.join(pwd, 'packages'))
2014-06-10 00:18:30 +04:00
2015-03-10 01:23:29 +03:00
from .__about__ import __version__
2017-02-21 03:18:38 +03:00
from .arguments import parseArguments
from .compat import u, is_py3
2016-03-06 23:48:42 +03:00
from .constants import (
API_ERROR,
AUTH_ERROR,
SUCCESS,
2016-03-07 01:13:27 +03:00
UNKNOWN_ERROR,
2016-04-29 01:04:46 +03:00
MALFORMED_HEARTBEAT_ERROR,
2016-03-06 23:48:42 +03:00
)
2015-04-04 21:03:55 +03:00
from .logger import setup_logging
2017-05-25 09:53:28 +03:00
log = logging.getLogger('WakaTime')
try:
from .packages import requests
except ImportError:
log.traceback(logging.ERROR)
print(traceback.format_exc())
log.error('Please upgrade Python to the latest version.')
print('Please upgrade Python to the latest version.')
sys.exit(UNKNOWN_ERROR)
2015-05-13 01:03:23 +03:00
from .offlinequeue import Queue
2015-03-10 01:23:29 +03:00
from .packages.requests.exceptions import RequestException
2015-06-30 05:47:04 +03:00
from .project import get_project_info
2015-05-13 01:03:23 +03:00
from .session_cache import SessionCache
from .stats import get_file_stats
2017-02-21 03:18:38 +03:00
from .utils import get_user_agent, should_exclude, format_file_path
2014-06-10 00:18:30 +04:00
try:
2015-08-25 10:42:37 +03:00
from .packages import simplejson as json # pragma: nocover
2015-09-29 13:11:25 +03:00
except (ImportError, SyntaxError): # pragma: nocover
import json
2016-04-19 01:26:28 +03:00
from .packages import tzlocal
2016-03-06 23:48:42 +03:00
def send_heartbeat(project=None, branch=None, hostname=None, stats={}, key=None,
2016-04-29 01:04:46 +03:00
entity=None, timestamp=None, is_write=None, plugin=None,
2016-03-06 23:48:42 +03:00
offline=None, entity_type='file', hidefilenames=None,
2017-05-25 09:53:28 +03:00
proxy=None, nosslverify=None, api_url=None, timeout=None,
use_ntlm_proxy=False, **kwargs):
2015-04-04 21:03:55 +03:00
"""Sends heartbeat as POST request to WakaTime api server.
2016-03-06 23:48:42 +03:00
Returns `SUCCESS` when heartbeat was sent, otherwise returns an
error code constant.
2015-04-04 21:03:55 +03:00
"""
if not api_url:
2016-01-07 00:57:03 +03:00
api_url = 'https://api.wakatime.com/api/v1/heartbeats'
2015-09-29 13:11:25 +03:00
if not timeout:
2016-04-29 01:04:46 +03:00
timeout = 60
2015-04-04 21:03:55 +03:00
log.debug('Sending heartbeat to api at %s' % api_url)
data = {
'time': timestamp,
2015-09-29 13:11:25 +03:00
'entity': entity,
'type': entity_type,
}
2015-09-29 13:11:25 +03:00
if hidefilenames and entity is not None and entity_type == 'file':
2017-02-09 06:25:31 +03:00
for pattern in hidefilenames:
try:
compiled = re.compile(pattern, re.IGNORECASE)
if compiled.search(entity):
extension = u(os.path.splitext(data['entity'])[1])
data['entity'] = u('HIDDEN{0}').format(extension)
break
except re.error as ex:
log.warning(u('Regex error ({msg}) for include pattern: {pattern}').format(
msg=u(ex),
pattern=u(pattern),
))
2013-09-23 00:51:23 +04:00
if stats.get('lines'):
data['lines'] = stats['lines']
if stats.get('language'):
data['language'] = stats['language']
2014-12-22 10:01:09 +03:00
if stats.get('dependencies'):
data['dependencies'] = stats['dependencies']
2015-05-07 01:45:34 +03:00
if stats.get('lineno'):
data['lineno'] = stats['lineno']
2015-05-07 02:33:32 +03:00
if stats.get('cursorpos'):
data['cursorpos'] = stats['cursorpos']
2016-04-29 01:04:46 +03:00
if is_write:
data['is_write'] = is_write
if project:
data['project'] = project
if branch:
data['branch'] = branch
log.debug(data)
# setup api request
2014-09-30 20:27:35 +04:00
request_body = json.dumps(data)
2015-03-10 01:23:29 +03:00
api_key = u(base64.b64encode(str.encode(key) if is_py3 else key))
auth = u('Basic {api_key}').format(api_key=api_key)
headers = {
'User-Agent': get_user_agent(plugin),
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': auth,
}
if hostname:
2016-03-07 01:13:27 +03:00
headers['X-Machine-Name'] = u(hostname).encode('utf-8')
2014-12-01 09:21:46 +03:00
2013-10-01 08:57:30 +04:00
# add Olson timezone to request
try:
tz = tzlocal.get_localzone()
except:
tz = None
2013-10-01 08:57:30 +04:00
if tz:
2016-03-07 01:13:27 +03:00
headers['TimeZone'] = u(tz.zone).encode('utf-8')
2013-10-01 08:57:30 +04:00
2015-05-13 01:03:23 +03:00
session_cache = SessionCache()
session = session_cache.get()
2017-05-25 09:53:28 +03:00
should_try_ntlm = False
2017-02-21 03:18:38 +03:00
proxies = {}
if proxy:
2017-05-25 09:53:28 +03:00
if use_ntlm_proxy:
2017-02-21 03:18:38 +03:00
from .packages.requests_ntlm import HttpNtlmAuth
username = proxy.rsplit(':', 1)
password = ''
if len(username) == 2:
password = username[1]
username = username[0]
session.auth = HttpNtlmAuth(username, password, session)
else:
2017-05-25 09:53:28 +03:00
should_try_ntlm = '\\' in proxy
2017-02-21 03:18:38 +03:00
proxies['https'] = proxy
2017-05-25 09:53:28 +03:00
# send request to api
2013-09-07 09:50:12 +04:00
response = None
try:
2015-05-13 01:03:23 +03:00
response = session.post(api_url, data=request_body, headers=headers,
2017-05-25 09:53:28 +03:00
proxies=proxies, timeout=timeout,
verify=not nosslverify)
2015-03-10 01:23:29 +03:00
except RequestException:
2017-05-25 09:53:28 +03:00
if should_try_ntlm:
return send_heartbeat(
project=project,
entity=entity,
timestamp=timestamp,
branch=branch,
hostname=hostname,
stats=stats,
key=key,
is_write=is_write,
plugin=plugin,
offline=offline,
hidefilenames=hidefilenames,
entity_type=entity_type,
proxy=proxy,
api_url=api_url,
timeout=timeout,
use_ntlm_proxy=True,
)
else:
2017-05-25 09:53:28 +03:00
exception_data = {
sys.exc_info()[0].__name__: u(sys.exc_info()[1]),
}
if log.isEnabledFor(logging.DEBUG):
exception_data['traceback'] = traceback.format_exc()
if offline:
queue = Queue()
queue.push(data, json.dumps(stats), plugin)
if log.isEnabledFor(logging.DEBUG):
log.warn(exception_data)
else:
log.error(exception_data)
2016-07-07 00:25:21 +03:00
except: # delete cached session when requests raises unknown exception
2017-05-25 09:53:28 +03:00
if should_try_ntlm:
return send_heartbeat(
project=project,
entity=entity,
timestamp=timestamp,
branch=branch,
hostname=hostname,
stats=stats,
key=key,
is_write=is_write,
plugin=plugin,
offline=offline,
hidefilenames=hidefilenames,
entity_type=entity_type,
proxy=proxy,
api_url=api_url,
timeout=timeout,
use_ntlm_proxy=True,
)
else:
exception_data = {
sys.exc_info()[0].__name__: u(sys.exc_info()[1]),
'traceback': traceback.format_exc(),
}
if offline:
queue = Queue()
queue.push(data, json.dumps(stats), plugin)
log.warn(exception_data)
session_cache.delete()
return API_ERROR
2016-07-07 00:25:21 +03:00
else:
2016-03-06 23:48:42 +03:00
code = response.status_code if response is not None else None
content = response.text if response is not None else None
if code == requests.codes.created or code == requests.codes.accepted:
log.debug({
2016-03-06 23:48:42 +03:00
'response_code': code,
})
2015-05-13 01:03:23 +03:00
session_cache.save(session)
2016-03-06 23:48:42 +03:00
return SUCCESS
2017-05-25 09:53:28 +03:00
if should_try_ntlm:
return send_heartbeat(
project=project,
entity=entity,
timestamp=timestamp,
branch=branch,
hostname=hostname,
stats=stats,
key=key,
is_write=is_write,
plugin=plugin,
offline=offline,
hidefilenames=hidefilenames,
entity_type=entity_type,
proxy=proxy,
api_url=api_url,
timeout=timeout,
use_ntlm_proxy=True,
)
else:
if offline:
if code != 400:
queue = Queue()
queue.push(data, json.dumps(stats), plugin)
if code == 401:
log.error({
'response_code': code,
'response_content': content,
})
session_cache.delete()
return AUTH_ERROR
elif log.isEnabledFor(logging.DEBUG):
log.warn({
'response_code': code,
'response_content': content,
})
else:
2015-03-10 01:23:29 +03:00
log.error({
2016-03-06 23:48:42 +03:00
'response_code': code,
'response_content': content,
2015-03-10 01:23:29 +03:00
})
else:
log.error({
2016-03-06 23:48:42 +03:00
'response_code': code,
'response_content': content,
})
2015-05-13 01:03:23 +03:00
session_cache.delete()
2016-03-06 23:48:42 +03:00
return API_ERROR
def sync_offline_heartbeats(args, hostname):
"""Sends all heartbeats which were cached in the offline Queue."""
queue = Queue()
while True:
heartbeat = queue.pop()
if heartbeat is None:
break
status = send_heartbeat(
project=heartbeat['project'],
entity=heartbeat['entity'],
timestamp=heartbeat['time'],
branch=heartbeat['branch'],
hostname=hostname,
stats=json.loads(heartbeat['stats']),
key=args.key,
2016-04-29 01:04:46 +03:00
is_write=heartbeat['is_write'],
2016-03-06 23:48:42 +03:00
plugin=heartbeat['plugin'],
offline=args.offline,
hidefilenames=args.hidefilenames,
entity_type=heartbeat['type'],
proxy=args.proxy,
api_url=args.api_url,
timeout=args.timeout,
)
if status != SUCCESS:
if status == AUTH_ERROR:
return AUTH_ERROR
break
return SUCCESS
2016-04-29 01:04:46 +03:00
def process_heartbeat(args, configs, hostname, heartbeat):
exclude = should_exclude(heartbeat['entity'], args.include, args.exclude)
if exclude is not False:
log.debug(u('Skipping because matches exclude pattern: {pattern}').format(
pattern=u(exclude),
))
return SUCCESS
if heartbeat.get('entity_type') not in ['file', 'domain', 'app']:
heartbeat['entity_type'] = 'file'
2016-09-22 11:26:57 +03:00
if heartbeat['entity_type'] == 'file':
heartbeat['entity'] = format_file_path(heartbeat['entity'])
2016-04-29 01:04:46 +03:00
if heartbeat['entity_type'] != 'file' or os.path.isfile(heartbeat['entity']):
stats = get_file_stats(heartbeat['entity'],
entity_type=heartbeat['entity_type'],
lineno=heartbeat.get('lineno'),
cursorpos=heartbeat.get('cursorpos'),
plugin=args.plugin,
2017-02-21 03:18:38 +03:00
language=heartbeat.get('language'))
2016-04-29 01:04:46 +03:00
project = heartbeat.get('project') or heartbeat.get('alternate_project')
branch = None
if heartbeat['entity_type'] == 'file':
project, branch = get_project_info(configs, heartbeat)
heartbeat['project'] = project
heartbeat['branch'] = branch
heartbeat['stats'] = stats
heartbeat['hostname'] = hostname
heartbeat['timeout'] = args.timeout
heartbeat['key'] = args.key
2016-04-29 02:13:01 +03:00
heartbeat['plugin'] = args.plugin
heartbeat['offline'] = args.offline
heartbeat['hidefilenames'] = args.hidefilenames
heartbeat['proxy'] = args.proxy
2017-05-25 09:53:28 +03:00
heartbeat['nosslverify'] = args.nosslverify
2016-04-29 02:13:01 +03:00
heartbeat['api_url'] = args.api_url
2016-04-29 01:04:46 +03:00
return send_heartbeat(**heartbeat)
else:
log.debug('File does not exist; ignoring this heartbeat.')
return SUCCESS
2015-09-29 13:11:25 +03:00
def execute(argv=None):
if argv:
sys.argv = ['wakatime'] + argv
2015-08-25 10:42:37 +03:00
args, configs = parseArguments()
setup_logging(args, __version__)
2016-01-07 00:57:03 +03:00
try:
2016-04-29 01:04:46 +03:00
hostname = args.hostname or socket.gethostname()
heartbeat = vars(args)
retval = process_heartbeat(args, configs, hostname, heartbeat)
if args.extra_heartbeats:
try:
for heartbeat in json.loads(sys.stdin.readline()):
retval = process_heartbeat(args, configs, hostname, heartbeat)
except json.JSONDecodeError:
retval = MALFORMED_HEARTBEAT_ERROR
if retval == SUCCESS:
retval = sync_offline_heartbeats(args, hostname)
return retval
2016-01-07 00:57:03 +03:00
except:
2016-09-02 11:50:54 +03:00
log.traceback(logging.ERROR)
2016-04-29 01:04:46 +03:00
print(traceback.format_exc())
2016-03-07 01:13:27 +03:00
return UNKNOWN_ERROR