1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# Copyright (c) 2022 Huawei Device Co., Ltd.
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import os
17import json
18import copy
19import sa_info_config_errors as json_err
20
21
22class RearrangementPolicy(object):
23    BOOT_START_PHASE = "BootStartPhase"
24    CORE_START_PHASE = "CoreStartPhase"
25    DEFAULT_START_PHASE = "OthersStartPhase"
26
27    rearrange_category_order = (BOOT_START_PHASE, CORE_START_PHASE,
28                                DEFAULT_START_PHASE)
29
30    bootphase_priority_table = {
31        BOOT_START_PHASE: 3,
32        CORE_START_PHASE: 2,
33        DEFAULT_START_PHASE: 1
34    }
35
36    def __init__(self):
37        self.bootphase_categories = {
38            RearrangementPolicy.BOOT_START_PHASE: [],
39            RearrangementPolicy.CORE_START_PHASE: [],
40            RearrangementPolicy.DEFAULT_START_PHASE: []
41        }
42
43
44class SARearrangement(object):
45    def __init__(self):
46        self.rearranged_systemabilities = []
47        self.ordered_systemability_names = []
48        self.name_node_dict = {}
49        self.systemability_deps_dict = {}
50        self.bootphase_dict = {}
51        self.creation_dict = {}
52        self.policy = RearrangementPolicy()
53        self.sanode_start_idx = 0
54        self.systemability_nodes = []
55        self.sa_nodes_count = 0
56
57    @classmethod
58    def detect_invalid_dependency_globally(clazz,
59                                           global_ordered_systemability_names,
60                                           global_systemability_deps_dict):
61        dependency_checkers = []
62        clazz.__detect_invalid_dependency(dependency_checkers,
63                                          global_ordered_systemability_names,
64                                          global_systemability_deps_dict)
65
66    @classmethod
67    def __detect_invalid_dependency(self, dependency_checkers, ordered_sa_names: str,
68                                    sa_deps_dict: dict):
69        """
70        Iterate over the dependency tree, to detect whether there
71        exists circular dependency and other kinds bad dependency
72        """
73        deps_visit_cnt = {}
74        ordered_systemability_names = ordered_sa_names
75        systemability_deps_dict = sa_deps_dict
76
77        def init_dependencies_visit_counter():
78            for name in ordered_systemability_names:
79                deps_visit_cnt[name] = 0
80
81        def do_check(systemability, dependency):
82            """
83            Check other kind dependency problem
84            """
85            for checker in dependency_checkers:
86                checker(systemability, dependency)
87
88        def check_depend(cur_systemability: str, deps_count: int, dependencies: list, depend_path: list):
89            if deps_visit_cnt.get(cur_systemability) < deps_count:
90                index = deps_visit_cnt.get(cur_systemability)
91                cur_dependency = dependencies[index]
92                # execute other kind dependency checkers right here
93                do_check(cur_systemability, cur_dependency)
94                try:
95                    depend_path.index(cur_dependency)
96                    depend_path.append(cur_dependency)
97                    _format = "A circular dependency found: {}"
98                    route = "->".join(map(str, depend_path))
99                    raise json_err.CircularDependencyError(_format.format(route))
100                except ValueError:
101                    depend_path.append(cur_dependency)
102                    deps_visit_cnt[cur_systemability] += 1
103            else:
104                # pop the systemability in process if it's all
105                # dependencies have been visited
106                depend_path.pop()
107
108        init_dependencies_visit_counter()
109        for systemability in ordered_systemability_names:
110            depend_path = []
111            depend_path.append(systemability)
112            while len(depend_path) != 0:
113                cur_systemability = depend_path[-1]
114                # the cur_systemability may be in a different process,
115                # thus can't find it's dependency info
116                dependencies = systemability_deps_dict.get(cur_systemability)
117                if dependencies is None:
118                    dependencies = []
119                deps_count = len(dependencies)
120                if deps_count == 0:
121                    depend_path.pop()
122                else:
123                    check_depend(cur_systemability, deps_count, dependencies, depend_path)
124
125    def sort(self, source_file: str, dest_file: str):
126        self.file_in_process = source_file
127        dependency_checkers = []
128        dependency_checkers.append(self.__detect_invert_dependency)
129        dependency_checkers.append(self.__detect_creation_dependency)
130
131        self.__parse_json_file(source_file)
132        self.__extract_info_from_systemability_nodes()
133        self.__detect_invalid_dependency(dependency_checkers,
134                                         self.ordered_systemability_names,
135                                         self.systemability_deps_dict)
136        self.__sort_systemability_by_bootphase_priority()
137        self.__rearrange_systemability_node_strict(source_file, dest_file)
138
139    def get_deps_info(self):
140        """
141        Returns systemabilities and their dependencies for later detecting
142        possible globally circular dependency problem
143        """
144        return [self.ordered_systemability_names, self.systemability_deps_dict]
145
146    def __detect_invert_dependency(self, systemability: list, depend):
147        """
148        Detect invert dependency: systemability with high boot priority depends
149        on systemability with low ones, e.g. a systemability named 'sa1' with
150        BootStartPhase priority depends on a systemability named 'sa2' with
151        CoreStartPhase
152        """
153        _format = ("Bad dependency found: the {} with high priority " +
154                   "depends on a {} with low one")
155        self_idx = self.bootphase_dict.get(systemability)
156        # The depend may be in other process
157        dep_idx = self.bootphase_dict.get(depend)
158        if dep_idx is None:
159            return
160        self_priority = RearrangementPolicy.bootphase_priority_table.get(
161            self_idx)
162        depend_priority = RearrangementPolicy.bootphase_priority_table.get(
163            dep_idx)
164        if self_priority > depend_priority:
165            raise json_err.InvertDependencyError(
166                _format.format(systemability, depend))
167
168    def __parse_json_file(self, source_file: str):
169        with open(source_file, "r", encoding="utf-8") as file:
170            data = json.load(file)
171        self.systemability_nodes = data['systemability']
172        try:
173            first_sa_node = self.systemability_nodes[0]
174            self.sa_nodes_count = len(self.systemability_nodes)
175        except IndexError:
176            pass
177
178    def __rearrange_systemability_node_strict(self, source_file: str, dest_file: str):
179        rearranged_name = self.rearranged_systemabilities
180        final_systemability = []
181        for name in rearranged_name:
182            temp = self.name_node_dict.get(name)
183            final_systemability.append(temp)
184        with open(source_file, "r", encoding="utf-8") as file:
185            data = json.load(file)
186        data['systemability'] = final_systemability
187        file_node = os.open(dest_file, os.O_RDWR | os.O_CREAT, 0o640)
188        with os.fdopen(file_node, 'w') as json_files:
189            json.dump(data, json_files, indent=4, ensure_ascii=False)
190
191    def __detect_creation_dependency(self, systemability: list, depend):
192        """
193        Detect dependency related to configuration on <run-on-create>:
194        if a sa with <run-on-create> set to 'true' depending on a sa
195        with 'false', then a RunOnCreateDependencyError will be thrown
196        """
197        _format = ("Bad dependency found: the {} with run-on-create " +
198                   "depends on a {} with run-on-demand")
199        self_creation = self.creation_dict.get(systemability)
200        dep_creation = self.creation_dict.get(depend)
201        if self_creation is True and dep_creation is False:
202            raise json_err.RunOnCreateDependencyError(_format.format(systemability, depend))
203
204    def __extract_info_from_systemability_nodes(self):
205        """
206        Extract info like dependencies and bootphase from a systemability node
207        """
208
209        def validate_creation(creation: bool):
210            _format = ("In tag {} only a boolean value is expected, " +
211                       "but actually is '{}'")
212            if str(creation) not in {"true", "false", "True", "False"}:
213                raise json_err.BadFormatJsonError(_format.format("run-on-create", creation),
214                                                  self.file_in_process)
215
216        def validate_bootphase(bootphase, nodename: str):
217            _format = ("In systemability: {}, The bootphase '{}' is not supported " +
218                       "please check yourself")
219            if self.policy.bootphase_categories.get(bootphase) is None:
220                raise json_err.NotSupportedBootphaseError(_format.format(nodename, bootphase))
221
222        def validate_systemability_name(nodename: str):
223            if nodename < 1 or nodename > 16777215:
224                _format = ("name's value should be [1-16777215], but actually is {}")
225                raise json_err.BadFormatJsonError(_format.format(nodename),
226                                                  self.file_in_process)
227
228        def check_nodes_constraints_one(systemability_node: dict, tag: str):
229            _format = ("The tag {} should exist, but it does not exist")
230            if tag not in systemability_node:
231                raise json_err.BadFormatJsonError(_format.format(tag), self.file_in_process)
232            tags_nodes = systemability_node[tag]
233            return tags_nodes
234
235        def check_nodes_constraints_two(systemability_node: dict, tag: str):
236            if tag not in systemability_node or systemability_node[tag] == '':
237                return ""
238            tags_nodes = systemability_node[tag]
239            return tags_nodes
240
241        default_bootphase = RearrangementPolicy.DEFAULT_START_PHASE
242        for systemability_node in self.systemability_nodes:
243            # Required <name> one and only one is expected
244            name_node = check_nodes_constraints_one(systemability_node, "name")
245            validate_systemability_name(name_node)
246            try:
247                self.ordered_systemability_names.index(name_node)
248                raise json_err.SystemAbilityNameConflictError(name_node)
249            except ValueError:
250                self.ordered_systemability_names.append(name_node)
251            self.name_node_dict[name_node] = systemability_node
252            self.systemability_deps_dict[name_node] = []
253            self.bootphase_dict[name_node] = default_bootphase
254            # Optional bootphase: zero or one are both accepted
255            bootphase_nodes = check_nodes_constraints_two(systemability_node,
256                                                          "bootphase")
257            if bootphase_nodes != '':
258                validate_bootphase(bootphase_nodes, name_node)
259                self.bootphase_dict[name_node] = bootphase_nodes
260            # Required run-on-create one and only one is expected
261            runoncreate_node = check_nodes_constraints_one(systemability_node,
262                                                           "run-on-create")
263            validate_creation(runoncreate_node)
264            self.creation_dict[name_node] = runoncreate_node
265            # Optional depend:
266            depend_nodes = check_nodes_constraints_two(systemability_node,
267                                                       "depend")
268            for depend_node in depend_nodes:
269                deps = self.systemability_deps_dict.get(name_node)
270                deps.append(depend_node)
271
272    def __sort_systemability_by_bootphase_priority(self):
273        def check_index(systemabilities: list, dependency, idx_self: int):
274            try:
275                idx_dep = systemabilities.index(dependency)
276                # if the dependency is behind, then exchange the order
277                if idx_self < idx_dep:
278                    tmp = systemabilities[idx_dep]
279                    systemabilities[idx_dep] = systemabilities[idx_self]
280                    systemabilities[idx_self] = tmp
281            except ValueError:
282                pass  # ignore different category of dependencies
283
284        def inner_category_sort(systemabilities: list):
285            """
286            Sort dependencies with same bootphase category, preserve the
287            original order in source file
288            """
289            systemabilities_ = systemabilities[:]
290            for systemability in systemabilities_:
291                dependencies = self.systemability_deps_dict.get(systemability)
292                for dependency in dependencies:
293                    # should update idx_self each iteration
294                    idx_self = systemabilities.index(systemability)
295                    check_index(systemabilities, dependency, idx_self)
296
297        # put the systemability nodes into different categories
298        for systemability_name in self.ordered_systemability_names:
299            bootphase = self.bootphase_dict.get(systemability_name)
300            salist = self.policy.bootphase_categories.get(bootphase)
301            salist.append(systemability_name)
302
303        # sort the systemability nodes according to RearrangementPolicy
304        for category in RearrangementPolicy.rearrange_category_order:
305            salist = self.policy.bootphase_categories.get(category)
306            inner_category_sort(salist)
307            self.rearranged_systemabilities += salist
308