Repository: sublimelinter/sublimelinter
Issue: util.cd is not thread aware

Due to async linting you can see something like this in action:

> Already patched
** chdir C:\\Users\\c-flo\\AppData\\Roaming\\Sublime Text 3\\Packages\\User** chdir C:\\Users\\c-flo\\AppData\\Roaming\\Sublime Text 3\\Packages\\SublimeLinter

SublimeLinter: flake8: wanted executable is 'C:\\Dev\\python35\\Scripts\\flake8.exe' 
** chdir C:\\Users\\c-flo\\AppData\\Roaming\\Sublime Text 3\\Packages\\SublimeLinter\\lint
SublimeLinter: flake8: util.py ['C:\\\\Dev\\\\python35\\\\Scripts\\\\flake8.exe', '-'] 

What do you think the current dir will be after all this? T.i. when https://github.com/SublimeLinter/SublimeLinter/blob/4cb3dd5903aa83e69b2c4a136c093fd2819eee59/lint/util.py#L604-L606 runs 😒 😶 

Changed files:
lint/base_linter/python_linter.py, lint/linter.py, lint/util.py

diff --git a/lint/base_linter/python_linter.py b/lint/base_linter/python_linter.py
index 2d8f52a..97718e0 100644
--- a/lint/base_linter/python_linter.py
+++ b/lint/base_linter/python_linter.py
@@ -259,8 +259,7 @@ def ask_pipenv(linter_name, chdir):
 @lru_cache(maxsize=None)
 def _ask_pipenv(linter_name, chdir):
     cmd = ['pipenv', '--venv']
-    with util.cd(chdir):
-        venv = _communicate(cmd).strip().split('\\n')[-1]
+    venv = _communicate(cmd, cwd=chdir).strip().split('\\n')[-1]
 
     if not venv:
         return
@@ -268,7 +267,7 @@ def _ask_pipenv(linter_name, chdir):
     return find_script_by_python_env(venv, linter_name)
 
 
-def _communicate(cmd):
+def _communicate(cmd, cwd):
     """Short wrapper around subprocess.check_output to eat all errors."""
     env = util.create_environment()
     info = None
@@ -281,7 +280,7 @@ def _communicate(cmd):
 
     try:
         return subprocess.check_output(
-            cmd, env=env, startupinfo=info, universal_newlines=True
+            cmd, env=env, startupinfo=info, universal_newlines=True, cwd=cwd
         )
     except Exception as err:
         persist.debug(
diff --git a/lint/linter.py b/lint/linter.py
index 45ab2c7..dd89796 100644
--- a/lint/linter.py
+++ b/lint/linter.py
@@ -953,7 +953,6 @@ class Linter(metaclass=LinterMeta):
         chdir = settings.get('chdir', None)
 
         if chdir and os.path.isdir(chdir):
-            persist.debug('chdir has been set to: {0}'.format(chdir))
             return chdir
         else:
             if self.filename:
@@ -985,11 +984,7 @@ class Linter(metaclass=LinterMeta):
             return []
 
         cmd = self.get_cmd()
-        settings = self.get_view_settings()
-        chdir = self.get_chdir(settings)
-
-        with util.cd(chdir):
-            output = self.run(cmd, code)
+        output = self.run(cmd, code)
 
         if not output:
             return []
@@ -1382,21 +1377,29 @@ class Linter(metaclass=LinterMeta):
         elif not code:
             cmd.append(self.filename)
 
+        settings = self.get_view_settings()
+        cwd = self.get_chdir(settings)
+
         return util.communicate(
             cmd,
             code,
             output_stream=self.error_stream,
-            env=self.env)
+            env=self.env,
+            cwd=cwd)
 
     def tmpfile(self, cmd, code, suffix=''):
         """Run an external executable using a temp file to pass code and return its output."""
+        settings = self.get_view_settings()
+        cwd = self.get_chdir(settings)
+
         return util.tmpfile(
             cmd,
             code,
             self.filename,
             suffix or self.get_tempfile_suffix(),
             output_stream=self.error_stream,
-            env=self.env)
+            env=self.env,
+            cwd=cwd)
 
     def tmpdir(self, cmd, files, code):
         """Run an external executable using a temp dir filled with files and return its output."""
diff --git a/lint/util.py b/lint/util.py
index 2635f74..49ee680 100644
--- a/lint/util.py
+++ b/lint/util.py
@@ -321,7 +321,7 @@ def combine_output(out, sep=''):
     return ANSI_COLOR_RE.sub('', output)
 
 
-def communicate(cmd, code=None, output_stream=STREAM_STDOUT, env=None):
+def communicate(cmd, code=None, output_stream=STREAM_STDOUT, env=None, cwd=None):
     """
     Return the result of sending code via stdin to an executable.
 
@@ -346,7 +346,7 @@ def communicate(cmd, code=None, output_stream=STREAM_STDOUT, env=None):
         stdout = stderr = None
 
     out = popen(cmd, stdout=stdout, stderr=stderr,
-                output_stream=output_stream, extra_env=env)
+                output_stream=output_stream, extra_env=env, cwd=cwd)
 
     if out is not None:
         if code is not None:
@@ -401,7 +401,7 @@ def create_tempdir():
     persist.debug('temp directory:', tempdir)
 
 
-def tmpfile(cmd, code, filename, suffix='', output_stream=STREAM_STDOUT, env=None):
+def tmpfile(cmd, code, filename, suffix='', output_stream=STREAM_STDOUT, env=None, cwd=None):
     """
     Return the result of running an executable against a temporary file containing code.
 
@@ -436,7 +436,7 @@ def tmpfile(cmd, code, filename, suffix='', output_stream=STREAM_STDOUT, env=Non
         else:
             cmd.append(path)
 
-        return communicate(cmd, output_stream=output_stream, env=env)
+        return communicate(cmd, output_stream=output_stream, env=env, cwd=cwd)
     finally:
         os.remove(path)
 
@@ -489,7 +489,7 @@ def tmpdir(cmd, files, filename, code, output_stream=STREAM_STDOUT, env=None):
     return out or ''
 
 
-def popen(cmd, stdout=None, stderr=None, output_stream=STREAM_BOTH, env=None, extra_env=None):
+def popen(cmd, stdout=None, stderr=None, output_stream=STREAM_BOTH, env=None, extra_env=None, cwd=None):
     """Open a pipe to an external process and return a Popen object."""
     info = None
 
@@ -521,7 +521,8 @@ def popen(cmd, stdout=None, stderr=None, output_stream=STREAM_BOTH, env=None, ex
             stdout=stdout,
             stderr=stderr,
             startupinfo=info,
-            env=env
+            env=env,
+            cwd=cwd,
         )
     except Exception as err:
         printf('ERROR: could not launch', repr(cmd))
@@ -589,23 +590,6 @@ def convert_type(value, type_value, sep=None, default=None):
     return default
 
 
-class cd:
-    """Context manager for changing the current working directory."""
-
-    def __init__(self, newPath):
-        """Save the new wd."""
-        self.newPath = os.path.expanduser(newPath)
-
-    def __enter__(self):
-        """Save the old wd and change to the new wd."""
-        self.savedPath = os.getcwd()
-        os.chdir(self.newPath)
-
-    def __exit__(self, etype, value, traceback):
-        """Go back to the old wd."""
-        os.chdir(self.savedPath)
-
-
 def load_json(*segments, from_sl_dir=False):
     base_path = "Packages/SublimeLinter/" if from_sl_dir else ""
     full_path = base_path + "/".join(segments)