diff --git a/src/azure-cli-core/azure/cli/core/auth/binary_cache.py b/src/azure-cli-core/azure/cli/core/auth/binary_cache.py index e56c95b130f..3b1d2aba500 100644 --- a/src/azure-cli-core/azure/cli/core/auth/binary_cache.py +++ b/src/azure-cli-core/azure/cli/core/auth/binary_cache.py @@ -4,7 +4,8 @@ # -------------------------------------------------------------------------------------------- from collections.abc import MutableMapping -import pickle +import json +import os from azure.cli.core.decorators import retry from knack.log import get_logger @@ -15,7 +16,7 @@ class BinaryCache(MutableMapping): """ Derived from azure.cli.core._session.Session. - A simple dict-like class that is backed by a binary file. + A simple dict-like class that is backed by a JSON file. All direct modifications with `__setitem__` and `__delitem__` will save the file. Indirect modifications should be followed by a call to `save`. @@ -31,8 +32,8 @@ def __init__(self, file_name): def _load(self): """Load cache with retry. If it still fails at last, raise the original exception as-is.""" try: - with open(self.filename, 'rb') as f: - return pickle.load(f) + with open(self.filename, 'r', encoding='utf-8') as f: + return json.load(f) except FileNotFoundError: # The cache file has not been created. This is expected. No need to retry. logger.debug("%s not found. Using a fresh one.", self.filename) @@ -44,19 +45,16 @@ def load(self): self.data = self._load() except Exception as ex: # pylint: disable=broad-exception-caught # If we still get exception after retry, ignore all types of exceptions and use a new cache. - # - pickle.UnpicklingError is caused by corrupted cache file, perhaps due to concurrent writes. - # - EOFError is caused by empty cache file created by other az instance, but hasn't been filled yet. - # - AttributeError is caused by reading cache generated by different MSAL versions. + # - json.JSONDecodeError is caused by corrupted or legacy pickle cache file. + # - ValueError/KeyError from malformed JSON content. logger.debug("Failed to load cache: %s. Using a fresh one.", ex) self.data = {} # Ignore a non-existing or corrupted http_cache @retry() def _save(self): - with open(self.filename, 'wb') as f: - # At this point, an empty cache file will be created. Loading this cache file will - # raise EOFError. This can be simulated by adding time.sleep(30) here. - # So during loading, EOFError is ignored. - pickle.dump(self.data, f) + fd = os.open(self.filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + with os.fdopen(fd, 'w', encoding='utf-8') as f: + json.dump(self.data, f) def save(self): logger.debug("save: %s", self.filename)