# -*- coding: utf-8 -*-
"""
A rough layout of the current storage goes as:

virtualenv-app-data
├── py - <version> <cache information about python interpreters>
│   └── *.json/lock
├── wheel <cache wheels used for seeding>
│   ├── house
│   │   └── *.whl <wheels downloaded go here>
│   └── <python major.minor> -> 3.9
│       ├── img-<version>
│       │   └── image
│       │           └── <install class> -> CopyPipInstall / SymlinkPipInstall
│       │               └── <wheel name> -> pip-20.1.1-py2.py3-none-any
│       └── embed
│           └── 1
│               └── *.json -> for every distribution contains data about newer embed versions and releases
└─── unzip <in zip app we cannot refer to some internal files, so first extract them>
     └── <virtualenv version>
         ├── py_info.py
         ├── debug.py
         └── _virtualenv.py
"""
from __future__ import absolute_import, unicode_literals

import json
import logging
from abc import ABCMeta
from contextlib import contextmanager
from hashlib import sha256

import six

from virtualenv.util.lock import ReentrantFileLock
from virtualenv.util.path import safe_delete
from virtualenv.util.six import ensure_text
from virtualenv.util.zipapp import extract
from virtualenv.version import __version__

from .base import AppData, ContentStore


class AppDataDiskFolder(AppData):
    """
    Store the application data on the disk within a folder layout.
    """

    def __init__(self, folder):
        self.lock = ReentrantFileLock(folder)

    def __repr__(self):
        return "{}".format(self.lock.path)

    @property
    def transient(self):
        return False

    def reset(self):
        logging.debug("reset app data folder %s", self.lock.path)
        safe_delete(self.lock.path)

    def close(self):
        """do nothing"""

    @contextmanager
    def locked(self, path):
        path_lock = self.lock / path
        with path_lock:
            yield path_lock.path

    @contextmanager
    def extract(self, path, to_folder):
        if to_folder is not None:
            root = ReentrantFileLock(to_folder())
        else:
            root = self.lock / "unzip" / __version__
        with root.lock_for_key(path.name):
            dest = root.path / path.name
            if not dest.exists():
                extract(path, dest)
            yield dest

    @property
    def py_info_at(self):
        return self.lock / "py_info" / "1"

    def py_info(self, path):
        return PyInfoStoreDisk(self.py_info_at, path)

    def py_info_clear(self):
        """"""
        py_info_folder = self.py_info_at
        with py_info_folder:
            for filename in py_info_folder.path.iterdir():
                if filename.suffix == ".json":
                    with py_info_folder.lock_for_key(filename.stem):
                        if filename.exists():
                            filename.unlink()

    def embed_update_log(self, distribution, for_py_version):
        return EmbedDistributionUpdateStoreDisk(self.lock / "wheel" / for_py_version / "embed" / "1", distribution)

    @property
    def house(self):
        path = self.lock.path / "wheel" / "house"
        path.mkdir(parents=True, exist_ok=True)
        return path

    def wheel_image(self, for_py_version, name):
        return self.lock.path / "wheel" / for_py_version / "image" / "1" / name


@six.add_metaclass(ABCMeta)
class JSONStoreDisk(ContentStore):
    def __init__(self, in_folder, key, msg, msg_args):
        self.in_folder = in_folder
        self.key = key
        self.msg = msg
        self.msg_args = msg_args + (self.file,)

    @property
    def file(self):
        return self.in_folder.path / "{}.json".format(self.key)

    def exists(self):
        return self.file.exists()

    def read(self):
        data, bad_format = None, False
        try:
            data = json.loads(self.file.read_text())
            logging.debug("got {} from %s".format(self.msg), *self.msg_args)
            return data
        except ValueError:
            bad_format = True
        except Exception:  # noqa
            pass
        if bad_format:
            self.remove()
        return None

    def remove(self):
        self.file.unlink()
        logging.debug("removed {} at %s".format(self.msg), *self.msg_args)

    @contextmanager
    def locked(self):
        with self.in_folder.lock_for_key(self.key):
            yield

    def write(self, content):
        folder = self.file.parent
        try:
            folder.mkdir(parents=True, exist_ok=True)
        except OSError:
            pass
        self.file.write_text(ensure_text(json.dumps(content, sort_keys=True, indent=2)))
        logging.debug("wrote {} at %s".format(self.msg), *self.msg_args)


class PyInfoStoreDisk(JSONStoreDisk):
    def __init__(self, in_folder, path):
        key = sha256(str(path).encode("utf-8") if six.PY3 else str(path)).hexdigest()
        super(PyInfoStoreDisk, self).__init__(in_folder, key, "python info of %s", (path,))


class EmbedDistributionUpdateStoreDisk(JSONStoreDisk):
    def __init__(self, in_folder, distribution):
        super(EmbedDistributionUpdateStoreDisk, self).__init__(
            in_folder,
            distribution,
            "embed update of distribution %s",
            (distribution,),
        )

src/virtualenv/app_data/via_disk_folder.py
# -*- coding: utf-8 -*-
"""
A rough layout of the current storage goes as:

virtualenv-app-data
├── py - <version> <cache information about python interpreters>
│   └── *.json/lock
├── wheel <cache wheels used for seeding>
│   ├── house
│   │   └── *.whl <wheels downloaded go here>
│   └── <python major.minor> -> 3.9
│       ├── img-<version>
│       │   └── image
│       │           └── <install class> -> CopyPipInstall / SymlinkPipInstall
│       │               └── <wheel name> -> pip-20.1.1-py2.py3-none-any
│       └── embed
│           └── 1
│               └── *.json -> for every distribution contains data about newer embed versions and releases
└─── unzip <in zip app we cannot refer to some internal files, so first extract them>
     └── <virtualenv version>
         ├── py_info.py
         ├── debug.py
         └── _virtualenv.py
"""
from __future__ import absolute_import, unicode_literals

import json
import logging
from abc import ABCMeta
from contextlib import contextmanager
from hashlib import sha256

import six

from virtualenv.util.lock import ReentrantFileLock
from virtualenv.util.path import safe_delete
from virtualenv.util.six import ensure_text
from virtualenv.util.zipapp import extract
from virtualenv.version import __version__

from .base import AppData, ContentStore


class AppDataDiskFolder(AppData):
    """
    Store the application data on the disk within a folder layout.
    """

    def __init__(self, folder):
        self.lock = ReentrantFileLock(folder)

    def __repr__(self):
        return "{}".format(self.lock.path)

    @property
    def transient(self):
        return False

    def reset(self):
        logging.debug("reset app data folder %s", self.lock.path)
        safe_delete(self.lock.path)

    def close(self):
        """do nothing"""

    @contextmanager
    def locked(self, path):
        path_lock = self.lock / path
        with path_lock:
            yield path_lock.path

    @contextmanager
    def extract(self, path, to_folder):
        if to_folder is not None:
            root = ReentrantFileLock(to_folder())
        else:
            root = self.lock / "unzip" / __version__
        with root.lock_for_key(path.name):
            dest = root.path / path.name
            if not dest.exists():
                extract(path, dest)
            yield dest

    @property
    def py_info_at(self):
        return self.lock / "py_info" / "1"

    def py_info(self, path):
        return PyInfoStoreDisk(self.py_info_at, path)

    def py_info_clear(self):
        """"""
        py_info_folder = self.py_info_at
        with py_info_folder:
            for filename in py_info_folder.path.iterdir():
                if filename.suffix == ".json":
                    with py_info_folder.lock_for_key(filename.stem):
                        if filename.exists():
                            filename.unlink()

    def embed_update_log(self, distribution, for_py_version):
        return EmbedDistributionUpdateStoreDisk(self.lock / "wheel" / for_py_version / "embed" / "1", distribution)

    @property
    def house(self):
        path = self.lock.path / "wheel" / "house"
        path.mkdir(parents=True, exist_ok=True)
        return path

    def wheel_image(self, for_py_version, name):
        return self.lock.path / "wheel" / for_py_version / "image" / "1" / name


@six.add_metaclass(ABCMeta)
class JSONStoreDisk(ContentStore):
    def __init__(self, in_folder, key, msg, msg_args):
        self.in_folder = in_folder
        self.key = key
        self.msg = msg
        self.msg_args = msg_args + (self.file,)

    @property
    def file(self):
        return self.in_folder.path / "{}.json".format(self.key)

    def exists(self):
        return self.file.exists()

    def read(self):
        data, bad_format = None, False
        try:
            data = json.loads(self.file.read_text())
            logging.debug("got {} from %s".format(self.msg), *self.msg_args)
            return data
        except ValueError:
            bad_format = True
        except Exception:  # noqa
            pass
        if bad_format:
            try:
                self.remove()
            except OSError:  # reading and writing on the same file may cause race on multiple processes
                pass
        return None

    def remove(self):
        self.file.unlink()
        logging.debug("removed {} at %s".format(self.msg), *self.msg_args)

    @contextmanager
    def locked(self):
        with self.in_folder.lock_for_key(self.key):
            yield

    def write(self, content):
        folder = self.file.parent
        try:
            folder.mkdir(parents=True, exist_ok=True)
        except OSError:
            pass
        self.file.write_text(ensure_text(json.dumps(content, sort_keys=True, indent=2)))
        logging.debug("wrote {} at %s".format(self.msg), *self.msg_args)


class PyInfoStoreDisk(JSONStoreDisk):
    def __init__(self, in_folder, path):
        key = sha256(str(path).encode("utf-8") if six.PY3 else str(path)).hexdigest()
        super(PyInfoStoreDisk, self).__init__(in_folder, key, "python info of %s", (path,))


class EmbedDistributionUpdateStoreDisk(JSONStoreDisk):
    def __init__(self, in_folder, distribution):
        super(EmbedDistributionUpdateStoreDisk, self).__init__(
            in_folder,
            distribution,
            "embed update of distribution %s",
            (distribution,),
        )

src/virtualenv/app_data/via_disk_folder.py
diff --git a/docs/changelog/1938.bugfix.rst b/docs/changelog/1938.bugfix.rst
new file mode 100644
index 000000000..0521c84d5
--- /dev/null
+++ b/docs/changelog/1938.bugfix.rst
@@ -0,0 +1 @@
+Fix a bug that reading and writing on the same file may cause race on multiple processes.
diff --git a/src/virtualenv/app_data/via_disk_folder.py b/src/virtualenv/app_data/via_disk_folder.py
index 4b2cadf20..55497e8e3 100644
--- a/src/virtualenv/app_data/via_disk_folder.py
+++ b/src/virtualenv/app_data/via_disk_folder.py
@@ -137,7 +137,10 @@ def read(self):
         except Exception:  # noqa
             pass
         if bad_format:
-            self.remove()
+            try:
+                self.remove()
+            except OSError:  # reading and writing on the same file may cause race on multiple processes
+                pass
         return None
 
     def remove(self):

Multiple processes competition on read/write update log
**Issue**

https://github.com/pypa/virtualenv/blob/0cd009b5a1338f66397f71c85a75f576a2f3eabf/src/virtualenv/app_data/via_disk_folder.py#L129-L141

When the file is opened for writing in another process, the JSON content can't be parsed, then the file will be removed. However, on Windows the removal will fail since the file is occupied by anther process.

**Environment**

Provide at least:
- OS: Windows 10
- ``pip list`` of the host python where ``virtualenv`` is installed:

  ```console
  appdirs           1.4.4
  argcomplete       1.11.1
  backcall          0.1.0
  better-exceptions 0.2.2
  certifi           2020.4.5.1
  chardet           3.0.4
  colorama          0.4.3
  decorator         4.4.2
  distlib           0.3.1
  filelock          3.0.12
  idna              2.10
  ipython           7.14.0
  ipython-genutils  0.2.0
  jedi              0.17.0
  naipyext          0.5.2
  parso             0.7.0
  pickleshare       0.7.5
  pip               20.1.1
  pipenv            2020.8.13.dev0 d:\workspace\pipenv
  pipx              0.15.1.3
  prompt-toolkit    3.0.5
  Pygments          2.6.1
  requests          2.24.0
  setuptools        46.4.0
  six               1.14.0
  traitlets         4.3.3
  urllib3           1.25.10
  userpath          1.3.0
  virtualenv        20.0.31
  virtualenv-clone  0.5.4
  wcwidth           0.1.9
  wheel             0.34.2
  ```

**Output of the virtual environment creation**

Make sure to run the creation with `-vvv --with-traceback`:

```pytb
Traceback (most recent call last):
  File "C:\Users\runneradmin\.virtualenvs\pipenv-6Kr0DpZ2\lib\site-packages\virtualenv\seed\embed\via_app_data\via_app_data.py", line 94, in _get
    do_periodic_update=self.periodic_update,
  File "C:\Users\runneradmin\.virtualenvs\pipenv-6Kr0DpZ2\lib\site-packages\virtualenv\seed\wheels\acquire.py", line 25, in get_wheel
    wheel = from_bundle(distribution, version, for_py_version, search_dirs, app_data, do_periodic_update)
  File "C:\Users\runneradmin\.virtualenvs\pipenv-6Kr0DpZ2\lib\site-packages\virtualenv\seed\wheels\bundle.py", line 20, in from_bundle
    wheel = periodic_update(distribution, for_py_version, wheel, search_dirs, app_data, do_periodic_update)
  File "C:\Users\runneradmin\.virtualenvs\pipenv-6Kr0DpZ2\lib\site-packages\virtualenv\seed\wheels\periodic_update.py", line 41, in periodic_update
    handle_auto_update(distribution, for_py_version, wheel, search_dirs, app_data)
  File "C:\Users\runneradmin\.virtualenvs\pipenv-6Kr0DpZ2\lib\site-packages\virtualenv\seed\wheels\periodic_update.py", line 62, in handle_auto_update
    u_log = UpdateLog.from_dict(embed_update_log.read())
  File "C:\Users\runneradmin\.virtualenvs\pipenv-6Kr0DpZ2\lib\site-packages\virtualenv\app_data\via_disk_folder.py", line 140, in read
    self.remove()
  File "C:\Users\runneradmin\.virtualenvs\pipenv-6Kr0DpZ2\lib\site-packages\virtualenv\app_data\via_disk_folder.py", line 144, in remove
    self.file.unlink()
  File "C:\hostedtoolcache\windows\Python\3.6.8\x64\lib\pathlib.py", line 1284, in unlink
    self._accessor.unlink(self)
  File "C:\hostedtoolcache\windows\Python\3.6.8\x64\lib\pathlib.py", line 387, in wrapped
    return strfunc(str(pathobj), *args)
PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'C:\\Users\\runneradmin\\AppData\\Local\\pypa\\virtualenv\\wheel\\3.8\\embed\\1\\wheel.json'
PermissionError(13, 'The process cannot access the file because it is being used by another process')
RuntimeError: seed failed due to failing to download wheels wheel
```

To fix the issue, I prefer to change the writing into **atomic**, that is, before the writing is done, the content should be kept.

If that is an acceptable approach I can send a PR.

