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