1# -------------------------------------------------------------------------------------------- 2# Copyright (c) Microsoft Corporation. All rights reserved. 3# Licensed under the MIT License. See License.txt in the project root for license information. 4# -------------------------------------------------------------------------------------------- 5 6import json 7import logging 8import os 9import time 10 11try: 12 import collections.abc as collections 13except ImportError: 14 import collections 15 16from codecs import open as codecs_open 17 18from knack.log import get_logger 19 20try: 21 t_JSONDecodeError = json.JSONDecodeError 22except AttributeError: # in Python 2.7 23 t_JSONDecodeError = ValueError 24 25 26class Session(collections.MutableMapping): 27 """ 28 A simple dict-like class that is backed by a JSON file. 29 30 All direct modifications will save the file. Indirect modifications should 31 be followed by a call to `save_with_retry` or `save`. 32 """ 33 34 def __init__(self, encoding=None): 35 super(Session, self).__init__() 36 self.filename = None 37 self.data = {} 38 self._encoding = encoding if encoding else 'utf-8-sig' 39 40 def load(self, filename, max_age=0): 41 self.filename = filename 42 self.data = {} 43 try: 44 if max_age > 0: 45 st = os.stat(self.filename) 46 if st.st_mtime + max_age < time.time(): 47 self.save() 48 with codecs_open(self.filename, 'r', encoding=self._encoding) as f: 49 self.data = json.load(f) 50 except (OSError, IOError, t_JSONDecodeError) as load_exception: 51 # OSError / IOError should imply file not found issues which are expected on fresh runs (e.g. on build 52 # agents or new systems). A parse error indicates invalid/bad data in the file. We do not wish to warn 53 # on missing files since we expect that, but do if the data isn't parsing as expected. 54 log_level = logging.INFO 55 if isinstance(load_exception, t_JSONDecodeError): 56 log_level = logging.WARNING 57 58 get_logger(__name__).log(log_level, 59 "Failed to load or parse file %s. It will be overridden by default settings.", 60 self.filename) 61 self.save() 62 63 def save(self): 64 if self.filename: 65 with codecs_open(self.filename, 'w', encoding=self._encoding) as f: 66 json.dump(self.data, f) 67 68 def save_with_retry(self, retries=5): 69 for _ in range(retries - 1): 70 try: 71 self.save() 72 break 73 except OSError: 74 time.sleep(0.1) 75 else: 76 self.save() 77 78 def get(self, key, default=None): 79 return self.data.get(key, default) 80 81 def __getitem__(self, key): 82 return self.data.setdefault(key, {}) 83 84 def __setitem__(self, key, value): 85 self.data[key] = value 86 self.save_with_retry() 87 88 def __delitem__(self, key): 89 del self.data[key] 90 self.save_with_retry() 91 92 def __iter__(self): 93 return iter(self.data) 94 95 def __len__(self): 96 return len(self.data) 97 98 99# ACCOUNT contains subscriptions information 100ACCOUNT = Session() 101 102# CONFIG provides external configuration options 103CONFIG = Session() 104 105# SESSION provides read-write session variables 106SESSION = Session() 107 108# INDEX contains {top-level command: [command_modules and extensions]} mapping index 109INDEX = Session() 110 111# VERSIONS provides local versions and pypi versions. 112# DO NOT USE it to get the current version of azure-cli, 113# it could be lagged behind and can be used to check whether 114# an upgrade of azure-cli happens 115VERSIONS = Session() 116 117# EXT_CMD_TREE provides command to extension name mapping 118EXT_CMD_TREE = Session() 119 120# CLOUD_ENDPOINTS provides endpoints/suffixes of clouds 121CLOUD_ENDPOINTS = Session() 122