confserver.py 12 KB

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