From 6b678942f072b4abfcb77bc812a5818b5c2867ea Mon Sep 17 00:00:00 2001 From: Alan Hamlett Date: Wed, 3 Oct 2018 00:28:16 -0700 Subject: [PATCH] detect Windows UNC prefix for remote network drives --- tests/samples/netuse/v1 | 7 +++ tests/samples/netuse/v2 | 8 ++++ tests/test_heartbeat.py | 30 +++++++++++- wakatime/compat.py | 6 ++- wakatime/heartbeat.py | 82 ++++++++++++++++++++++++++++++++- wakatime/projects/subversion.py | 12 ++--- 6 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 tests/samples/netuse/v1 create mode 100644 tests/samples/netuse/v2 diff --git a/tests/samples/netuse/v1 b/tests/samples/netuse/v1 new file mode 100644 index 0000000..a1c7172 --- /dev/null +++ b/tests/samples/netuse/v1 @@ -0,0 +1,7 @@ +New connections will be remembered. + +Status Local Remote Network + +------------------------------------------------------------------------------- + C: \\vboxsrv\Projects VirtualBox Shared Folders +The command completed successfully. diff --git a/tests/samples/netuse/v2 b/tests/samples/netuse/v2 new file mode 100644 index 0000000..827ba3a --- /dev/null +++ b/tests/samples/netuse/v2 @@ -0,0 +1,8 @@ +New connections will be remembered. + +Status Local Remote Network + +------------------------------------------------------------------------------- + C: \\vboxsrv\Projects VirtualBox Shared Folders +OK D: \\192.0.0.1\work VirtualBox Shared Folders +The command completed successfully. diff --git a/tests/test_heartbeat.py b/tests/test_heartbeat.py index 9d6bf0b..720f295 100644 --- a/tests/test_heartbeat.py +++ b/tests/test_heartbeat.py @@ -6,7 +6,7 @@ from wakatime.heartbeat import Heartbeat import os import logging from testfixtures import log_capture -from .utils import TestCase +from .utils import DynamicIterable, TestCase, mock class HeartbeatTestCase(TestCase): @@ -116,3 +116,31 @@ class HeartbeatTestCase(TestCase): self.assertNothingPrinted() self.assertNothingLogged(logs) + + def test_parsing(self): + + class Args(object): + hide_file_names = ['.*'] + hide_project_names = [] + plugin = None + + samples = [ + ('v1', 'C:\\v1\\file.txt', '\\\\vboxsrv\\Projects\\v1\\file.txt'), + ('v2', 'D:\\stuff\\v2\\file.py', '\\\\192.0.0.1\\work\\stuff\\v2\\file.py'), + ] + for sample, filepath, expected in samples: + with mock.patch('wakatime.heartbeat.Popen') as mock_popen: + + class MockCommunicate(object): + pass + + stdout = open('tests/samples/netuse/' + sample).read() + mock_communicate = MockCommunicate() + mock_communicate.communicate = mock.MagicMock(return_value=DynamicIterable((stdout, ''), max_calls=1)) + mock_popen.return_value = mock_communicate + + heartbeat = Heartbeat({'user_agent': 'test'}, Args(), None, _clone=True) + result = heartbeat._to_unc_path(filepath) + self.assertEquals(expected, result) + + self.assertNothingPrinted() diff --git a/wakatime/compat.py b/wakatime/compat.py index 744f3ee..18dc141 100644 --- a/wakatime/compat.py +++ b/wakatime/compat.py @@ -11,6 +11,7 @@ import codecs +import os import platform import subprocess import sys @@ -115,4 +116,7 @@ class Popen(subprocess.Popen): except AttributeError: pass kwargs['startupinfo'] = startupinfo - super(Popen, self).__init__(*args, **kwargs) + if 'env' not in kwargs: + kwargs['env'] = os.environ.copy() + kwargs['env']['LANG'] = 'en-US' if is_win else 'en_US.UTF-8' + subprocess.Popen.__init__(self, *args, **kwargs) diff --git a/wakatime/heartbeat.py b/wakatime/heartbeat.py index 73c25f5..7adbde0 100644 --- a/wakatime/heartbeat.py +++ b/wakatime/heartbeat.py @@ -10,8 +10,9 @@ import os import logging import re +from subprocess import PIPE -from .compat import u, json +from .compat import u, json, is_win, Popen from .exceptions import SkipHeartbeat from .project import get_project_info from .stats import get_file_stats @@ -85,6 +86,7 @@ class Heartbeat(object): return if self.type == 'file': self.entity = format_file_path(self.entity) + self._format_local_file() if not self._file_exists(): self.skip = u('File does not exist; ignoring this heartbeat.') return @@ -236,6 +238,84 @@ class Heartbeat(object): return (self.entity and os.path.isfile(self.entity) or self.args.local_file and os.path.isfile(self.args.local_file)) + def _format_local_file(self): + """When args.local_file empty on Windows, tries to map args.entity to a + unc path. + + Updates args.local_file in-place without returning anything. + """ + + if self.type != 'file': + return + + if not is_win: + return + + if self._file_exists(): + return + + self.args.local_file = self._to_unc_path(self.entity) + + def _to_unc_path(self, filepath): + drive, rest = self._splitdrive(filepath) + if not drive: + return filepath + + stdout = None + try: + stdout, stderr = Popen(['net', 'use'], stdout=PIPE, stderr=PIPE).communicate() + except OSError: + pass + else: + if stdout: + cols = None + for line in stdout.strip().splitlines()[1:]: + line = u(line) + if not line.strip(): + continue + if not cols: + cols = self._unc_columns(line) + continue + start, end = cols.get('local', (0, 0)) + if not start and not end: + break + local = line[start:end].strip().split(':')[0].upper() + if not local.isalpha(): + continue + if local == drive: + start, end = cols.get('remote', (0, 0)) + if not start and not end: + break + remote = line[start:end].strip() + return remote + rest + + return filepath + + def _unc_columns(self, line): + cols = {} + current_col = u('') + newcol = False + start, end = 0, 0 + for char in line: + if char.isalpha(): + if newcol: + cols[current_col.strip().lower()] = (start, end) + current_col = u('') + start = end + newcol = False + current_col += u(char) + else: + newcol = True + end += 1 + if start != end and current_col: + cols[current_col.strip().lower()] = (start, -1) + return cols + + def _splitdrive(self, filepath): + if filepath[1:2] != ':' or not filepath[0].isalpha(): + return None, filepath + return filepath[0].upper(), filepath[2:] + def _excluded_by_pattern(self): return should_exclude(self.entity, self.args.include, self.args.exclude) diff --git a/wakatime/projects/subversion.py b/wakatime/projects/subversion.py index d4f9572..ac6487b 100644 --- a/wakatime/projects/subversion.py +++ b/wakatime/projects/subversion.py @@ -70,17 +70,17 @@ class Subversion(BaseProject): if not self._is_mac() or self._has_xcode_tools(): stdout = None try: - os.environ['LANG'] = 'en_US' - stdout, stderr = Popen([ - self._find_binary(), 'info', os.path.realpath(path) - ], stdout=PIPE, stderr=PIPE).communicate() + stdout, stderr = Popen( + [self._find_binary(), 'info', os.path.realpath(path)], + stdout=PIPE, + stderr=PIPE, + ).communicate() except OSError: pass else: if stdout: for line in stdout.splitlines(): - line = u(line) - line = line.split(': ', 1) + line = u(line).split(': ', 1) if len(line) == 2: info[line[0]] = line[1] return info