confserver.py 12 KB

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