1#!/usr/bin/env python 2# 3# Long-running server process uses stdin & stdout to communicate JSON 4# with a caller 5# 6from __future__ import print_function 7 8import argparse 9import json 10import os 11import sys 12import tempfile 13 14import confgen 15import kconfiglib 16from confgen import FatalError, __version__ 17 18# Min/Max supported protocol versions 19MIN_PROTOCOL_VERSION = 1 20MAX_PROTOCOL_VERSION = 2 21 22 23def main(): 24 parser = argparse.ArgumentParser(description='confserver.py v%s - Config Generation Tool' % __version__, prog=os.path.basename(sys.argv[0])) 25 26 parser.add_argument('--config', 27 help='Project configuration settings', 28 required=True) 29 30 parser.add_argument('--kconfig', 31 help='KConfig file with config item definitions', 32 required=True) 33 34 parser.add_argument('--sdkconfig-rename', 35 help='File with deprecated Kconfig options', 36 required=False) 37 38 parser.add_argument('--env', action='append', default=[], 39 help='Environment to set when evaluating the config file', metavar='NAME=VAL') 40 41 parser.add_argument('--env-file', type=argparse.FileType('r'), 42 help='Optional file to load environment variables from. Contents ' 43 'should be a JSON object where each key/value pair is a variable.') 44 45 parser.add_argument('--version', help='Set protocol version to use on initial status', 46 type=int, default=MAX_PROTOCOL_VERSION) 47 48 args = parser.parse_args() 49 50 if args.version < MIN_PROTOCOL_VERSION: 51 print('Version %d is older than minimum supported protocol version %d. Client is much older than ESP-IDF version?' % 52 (args.version, MIN_PROTOCOL_VERSION)) 53 54 if args.version > MAX_PROTOCOL_VERSION: 55 print('Version %d is newer than maximum supported protocol version %d. Client is newer than ESP-IDF version?' % 56 (args.version, MAX_PROTOCOL_VERSION)) 57 58 try: 59 args.env = [(name,value) for (name,value) in (e.split('=',1) for e in args.env)] 60 except ValueError: 61 print("--env arguments must each contain =. To unset an environment variable, use 'ENV='") 62 sys.exit(1) 63 64 for name, value in args.env: 65 os.environ[name] = value 66 67 if args.env_file is not None: 68 env = json.load(args.env_file) 69 os.environ.update(confgen.dict_enc_for_env(env)) 70 71 run_server(args.kconfig, args.config, args.sdkconfig_rename) 72 73 74def run_server(kconfig, sdkconfig, sdkconfig_rename, default_version=MAX_PROTOCOL_VERSION): 75 config = kconfiglib.Kconfig(kconfig) 76 sdkconfig_renames = [sdkconfig_rename] if sdkconfig_rename else [] 77 sdkconfig_renames += os.environ.get('COMPONENT_SDKCONFIG_RENAMES', '').split() 78 deprecated_options = confgen.DeprecatedOptions(config.config_prefix, path_rename_files=sdkconfig_renames) 79 f_o = tempfile.NamedTemporaryFile(mode='w+b', delete=False) 80 try: 81 with open(sdkconfig, mode='rb') as f_i: 82 f_o.write(f_i.read()) 83 f_o.close() # need to close as DeprecatedOptions will reopen, and Windows only allows one open file 84 deprecated_options.replace(sdkconfig_in=f_o.name, sdkconfig_out=sdkconfig) 85 finally: 86 os.unlink(f_o.name) 87 config.load_config(sdkconfig) 88 89 print('Server running, waiting for requests on stdin...', file=sys.stderr) 90 91 config_dict = confgen.get_json_values(config) 92 ranges_dict = get_ranges(config) 93 visible_dict = get_visible(config) 94 95 if default_version == 1: 96 # V1: no 'visibility' key, send value None for any invisible item 97 values_dict = dict((k, v if visible_dict[k] else False) for (k,v) in config_dict.items()) 98 json.dump({'version': 1, 'values': values_dict, 'ranges': ranges_dict}, sys.stdout) 99 else: 100 # V2 onwards: separate visibility from version 101 json.dump({'version': default_version, 'values': config_dict, 'ranges': ranges_dict, 'visible': visible_dict}, sys.stdout) 102 print('\n') 103 sys.stdout.flush() 104 105 while True: 106 line = sys.stdin.readline() 107 if not line: 108 break 109 try: 110 req = json.loads(line) 111 except ValueError as e: # json module throws JSONDecodeError (sublcass of ValueError) on Py3 but ValueError on Py2 112 response = {'version': default_version, 'error': ['JSON formatting error: %s' % e]} 113 json.dump(response, sys.stdout) 114 print('\n') 115 sys.stdout.flush() 116 continue 117 before = confgen.get_json_values(config) 118 before_ranges = get_ranges(config) 119 before_visible = get_visible(config) 120 121 if 'load' in req: # load a new sdkconfig 122 123 if req.get('version', default_version) == 1: 124 # for V1 protocol, send all items when loading new sdkconfig. 125 # (V2+ will only send changes, same as when setting an item) 126 before = {} 127 before_ranges = {} 128 before_visible = {} 129 130 # if no new filename is supplied, use existing sdkconfig path, otherwise update the path 131 if req['load'] is None: 132 req['load'] = sdkconfig 133 else: 134 sdkconfig = req['load'] 135 136 if 'save' in req: 137 if req['save'] is None: 138 req['save'] = sdkconfig 139 else: 140 sdkconfig = req['save'] 141 142 error = handle_request(deprecated_options, config, req) 143 144 after = confgen.get_json_values(config) 145 after_ranges = get_ranges(config) 146 after_visible = get_visible(config) 147 148 values_diff = diff(before, after) 149 ranges_diff = diff(before_ranges, after_ranges) 150 visible_diff = diff(before_visible, after_visible) 151 if req['version'] == 1: 152 # V1 response, invisible items have value None 153 for k in (k for (k,v) in visible_diff.items() if not v): 154 values_diff[k] = None 155 response = {'version': 1, 'values': values_diff, 'ranges': ranges_diff} 156 else: 157 # V2+ response, separate visibility values 158 response = {'version': req['version'], 'values': values_diff, 'ranges': ranges_diff, 'visible': visible_diff} 159 if error: 160 for e in error: 161 print('Error: %s' % e, file=sys.stderr) 162 response['error'] = error 163 json.dump(response, sys.stdout) 164 print('\n') 165 sys.stdout.flush() 166 167 168def handle_request(deprecated_options, config, req): 169 if 'version' not in req: 170 return ["All requests must have a 'version'"] 171 172 if req['version'] < MIN_PROTOCOL_VERSION or req['version'] > MAX_PROTOCOL_VERSION: 173 return ['Unsupported request version %d. Server supports versions %d-%d' % ( 174 req['version'], 175 MIN_PROTOCOL_VERSION, 176 MAX_PROTOCOL_VERSION)] 177 178 error = [] 179 180 if 'load' in req: 181 print('Loading config from %s...' % req['load'], file=sys.stderr) 182 try: 183 config.load_config(req['load']) 184 except Exception as e: 185 error += ['Failed to load from %s: %s' % (req['load'], e)] 186 187 if 'set' in req: 188 handle_set(config, error, req['set']) 189 190 if 'save' in req: 191 try: 192 print('Saving config to %s...' % req['save'], file=sys.stderr) 193 confgen.write_config(deprecated_options, config, req['save']) 194 except Exception as e: 195 error += ['Failed to save to %s: %s' % (req['save'], e)] 196 197 return error 198 199 200def handle_set(config, error, to_set): 201 missing = [k for k in to_set if k not in config.syms] 202 if missing: 203 error.append('The following config symbol(s) were not found: %s' % (', '.join(missing))) 204 # replace name keys with the full config symbol for each key: 205 to_set = dict((config.syms[k],v) for (k,v) in to_set.items() if k not in missing) 206 207 # Work through the list of values to set, noting that 208 # some may not be immediately applicable (maybe they depend 209 # on another value which is being set). Therefore, defer 210 # knowing if any value is unsettable until then end 211 212 while len(to_set): 213 set_pass = [(k,v) for (k,v) in to_set.items() if k.visibility] 214 if not set_pass: 215 break # no visible keys left 216 for (sym,val) in set_pass: 217 if sym.type in (kconfiglib.BOOL, kconfiglib.TRISTATE): 218 if val is True: 219 sym.set_value(2) 220 elif val is False: 221 sym.set_value(0) 222 else: 223 error.append('Boolean symbol %s only accepts true/false values' % sym.name) 224 elif sym.type == kconfiglib.HEX: 225 try: 226 if not isinstance(val, int): 227 val = int(val, 16) # input can be a decimal JSON value or a string of hex digits 228 sym.set_value(hex(val)) 229 except ValueError: 230 error.append('Hex symbol %s can accept a decimal integer or a string of hex digits, only') 231 else: 232 sym.set_value(str(val)) 233 print('Set %s' % sym.name) 234 del to_set[sym] 235 236 if len(to_set): 237 error.append('The following config symbol(s) were not visible so were not updated: %s' % (', '.join(s.name for s in to_set))) 238 239 240def diff(before, after): 241 """ 242 Return a dictionary with the difference between 'before' and 'after', 243 for items which are present in 'after' dictionary 244 """ 245 diff = dict((k,v) for (k,v) in after.items() if before.get(k, None) != v) 246 return diff 247 248 249def get_ranges(config): 250 ranges_dict = {} 251 252 def is_base_n(i, n): 253 try: 254 int(i, n) 255 return True 256 except ValueError: 257 return False 258 259 def get_active_range(sym): 260 """ 261 Returns a tuple of (low, high) integer values if a range 262 limit is active for this symbol, or (None, None) if no range 263 limit exists. 264 """ 265 base = kconfiglib._TYPE_TO_BASE[sym.orig_type] if sym.orig_type in kconfiglib._TYPE_TO_BASE else 0 266 267 try: 268 for low_expr, high_expr, cond in sym.ranges: 269 if kconfiglib.expr_value(cond): 270 low = int(low_expr.str_value, base) if is_base_n(low_expr.str_value, base) else 0 271 high = int(high_expr.str_value, base) if is_base_n(high_expr.str_value, base) else 0 272 return (low, high) 273 except ValueError: 274 pass 275 return (None, None) 276 277 def handle_node(node): 278 sym = node.item 279 if not isinstance(sym, kconfiglib.Symbol): 280 return 281 active_range = get_active_range(sym) 282 if active_range[0] is not None: 283 ranges_dict[sym.name] = active_range 284 285 for n in config.node_iter(): 286 handle_node(n) 287 return ranges_dict 288 289 290def get_visible(config): 291 """ 292 Return a dict mapping node IDs (config names or menu node IDs) to True/False for their visibility 293 """ 294 result = {} 295 menus = [] 296 297 # when walking the menu the first time, only 298 # record whether the config symbols are visible 299 # and make a list of menu nodes (that are not symbols) 300 def handle_node(node): 301 sym = node.item 302 try: 303 visible = (sym.visibility != 0) 304 result[node] = visible 305 except AttributeError: 306 menus.append(node) 307 for n in config.node_iter(): 308 handle_node(n) 309 310 # now, figure out visibility for each menu. A menu is visible if any of its children are visible 311 for m in reversed(menus): # reverse to start at leaf nodes 312 result[m] = any(v for (n,v) in result.items() if n.parent == m) 313 314 # return a dict mapping the node ID to its visibility. 315 result = dict((confgen.get_menu_node_id(n),v) for (n,v) in result.items()) 316 317 return result 318 319 320if __name__ == '__main__': 321 try: 322 main() 323 except FatalError as e: 324 print('A fatal error occurred: %s' % e, file=sys.stderr) 325 sys.exit(2) 326