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