confserver.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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 confgen
  12. from confgen import FatalError, __version__
  13. try:
  14. from . import kconfiglib
  15. except Exception:
  16. sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
  17. import kconfiglib
  18. # Min/Max supported protocol versions
  19. MIN_PROTOCOL_VERSION = 1
  20. MAX_PROTOCOL_VERSION = 2
  21. def main():
  22. parser = argparse.ArgumentParser(description='confserver.py v%s - Config Generation Tool' % __version__, prog=os.path.basename(sys.argv[0]))
  23. parser.add_argument('--config',
  24. help='Project configuration settings',
  25. required=True)
  26. parser.add_argument('--kconfig',
  27. help='KConfig file with config item definitions',
  28. required=True)
  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('--version', help='Set protocol version to use on initial status',
  32. type=int, default=MAX_PROTOCOL_VERSION)
  33. args = parser.parse_args()
  34. if args.version < MIN_PROTOCOL_VERSION:
  35. print("Version %d is older than minimum supported protocol version %d. Client is much older than ESP-IDF version?" %
  36. (args.version, MIN_PROTOCOL_VERSION))
  37. if args.version > MAX_PROTOCOL_VERSION:
  38. print("Version %d is newer than maximum supported protocol version %d. Client is newer than ESP-IDF version?" %
  39. (args.version, MAX_PROTOCOL_VERSION))
  40. try:
  41. args.env = [(name,value) for (name,value) in (e.split("=",1) for e in args.env)]
  42. except ValueError:
  43. print("--env arguments must each contain =. To unset an environment variable, use 'ENV='")
  44. sys.exit(1)
  45. for name, value in args.env:
  46. os.environ[name] = value
  47. run_server(args.kconfig, args.config)
  48. def run_server(kconfig, sdkconfig, default_version=MAX_PROTOCOL_VERSION):
  49. config = kconfiglib.Kconfig(kconfig)
  50. config.load_config(sdkconfig)
  51. print("Server running, waiting for requests on stdin...", file=sys.stderr)
  52. config_dict = confgen.get_json_values(config)
  53. ranges_dict = get_ranges(config)
  54. visible_dict = get_visible(config)
  55. if default_version == 1:
  56. # V1: no 'visibility' key, send value None for any invisible item
  57. values_dict = dict((k, v if visible_dict[k] else False) for (k,v) in config_dict.items())
  58. json.dump({"version": 1, "values": values_dict, "ranges": ranges_dict}, sys.stdout)
  59. else:
  60. # V2 onwards: separate visibility from version
  61. json.dump({"version": default_version, "values": config_dict, "ranges": ranges_dict, "visible": visible_dict}, sys.stdout)
  62. print("\n")
  63. while True:
  64. line = sys.stdin.readline()
  65. if not line:
  66. break
  67. try:
  68. req = json.loads(line)
  69. except ValueError as e: # json module throws JSONDecodeError (sublcass of ValueError) on Py3 but ValueError on Py2
  70. response = {"version": default_version, "error": ["JSON formatting error: %s" % e]}
  71. json.dump(response, sys.stdout)
  72. print("\n")
  73. continue
  74. before = confgen.get_json_values(config)
  75. before_ranges = get_ranges(config)
  76. before_visible = get_visible(config)
  77. if "load" in req: # load a new sdkconfig
  78. if req.get("version", default_version) == 1:
  79. # for V1 protocol, send all items when loading new sdkconfig.
  80. # (V2+ will only send changes, same as when setting an item)
  81. before = {}
  82. before_ranges = {}
  83. before_visible = {}
  84. # if no new filename is supplied, use existing sdkconfig path, otherwise update the path
  85. if req["load"] is None:
  86. req["load"] = sdkconfig
  87. else:
  88. sdkconfig = req["load"]
  89. if "save" in req:
  90. if req["save"] is None:
  91. req["save"] = sdkconfig
  92. else:
  93. sdkconfig = req["save"]
  94. error = handle_request(config, req)
  95. after = confgen.get_json_values(config)
  96. after_ranges = get_ranges(config)
  97. after_visible = get_visible(config)
  98. values_diff = diff(before, after)
  99. ranges_diff = diff(before_ranges, after_ranges)
  100. visible_diff = diff(before_visible, after_visible)
  101. if req["version"] == 1:
  102. # V1 response, invisible items have value None
  103. for k in (k for (k,v) in visible_diff.items() if not v):
  104. values_diff[k] = None
  105. response = {"version": 1, "values": values_diff, "ranges": ranges_diff}
  106. else:
  107. # V2+ response, separate visibility values
  108. response = {"version": req["version"], "values": values_diff, "ranges": ranges_diff, "visible": visible_diff}
  109. if error:
  110. for e in error:
  111. print("Error: %s" % e, file=sys.stderr)
  112. response["error"] = error
  113. json.dump(response, sys.stdout)
  114. print("\n")
  115. def handle_request(config, req):
  116. if "version" not in req:
  117. return ["All requests must have a 'version'"]
  118. if req["version"] < MIN_PROTOCOL_VERSION or req["version"] > MAX_PROTOCOL_VERSION:
  119. return ["Unsupported request version %d. Server supports versions %d-%d" % (
  120. req["version"],
  121. MIN_PROTOCOL_VERSION,
  122. MAX_PROTOCOL_VERSION)]
  123. error = []
  124. if "load" in req:
  125. print("Loading config from %s..." % req["load"], file=sys.stderr)
  126. try:
  127. config.load_config(req["load"])
  128. except Exception as e:
  129. error += ["Failed to load from %s: %s" % (req["load"], e)]
  130. if "set" in req:
  131. handle_set(config, error, req["set"])
  132. if "save" in req:
  133. try:
  134. print("Saving config to %s..." % req["save"], file=sys.stderr)
  135. confgen.write_config(config, req["save"])
  136. except Exception as e:
  137. error += ["Failed to save to %s: %s" % (req["save"], e)]
  138. return error
  139. def handle_set(config, error, to_set):
  140. missing = [k for k in to_set if k not in config.syms]
  141. if missing:
  142. error.append("The following config symbol(s) were not found: %s" % (", ".join(missing)))
  143. # replace name keys with the full config symbol for each key:
  144. to_set = dict((config.syms[k],v) for (k,v) in to_set.items() if k not in missing)
  145. # Work through the list of values to set, noting that
  146. # some may not be immediately applicable (maybe they depend
  147. # on another value which is being set). Therefore, defer
  148. # knowing if any value is unsettable until then end
  149. while len(to_set):
  150. set_pass = [(k,v) for (k,v) in to_set.items() if k.visibility]
  151. if not set_pass:
  152. break # no visible keys left
  153. for (sym,val) in set_pass:
  154. if sym.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
  155. if val is True:
  156. sym.set_value(2)
  157. elif val is False:
  158. sym.set_value(0)
  159. else:
  160. error.append("Boolean symbol %s only accepts true/false values" % sym.name)
  161. else:
  162. sym.set_value(str(val))
  163. print("Set %s" % sym.name)
  164. del to_set[sym]
  165. if len(to_set):
  166. error.append("The following config symbol(s) were not visible so were not updated: %s" % (", ".join(s.name for s in to_set)))
  167. def diff(before, after):
  168. """
  169. Return a dictionary with the difference between 'before' and 'after',
  170. for items which are present in 'after' dictionary
  171. """
  172. diff = dict((k,v) for (k,v) in after.items() if before.get(k, None) != v)
  173. return diff
  174. def get_ranges(config):
  175. ranges_dict = {}
  176. def handle_node(node):
  177. sym = node.item
  178. if not isinstance(sym, kconfiglib.Symbol):
  179. return
  180. active_range = sym.active_range
  181. if active_range[0] is not None:
  182. ranges_dict[sym.name] = active_range
  183. config.walk_menu(handle_node)
  184. return ranges_dict
  185. def get_visible(config):
  186. """
  187. Return a dict mapping node IDs (config names or menu node IDs) to True/False for their visibility
  188. """
  189. result = {}
  190. menus = []
  191. # when walking the menu the first time, only
  192. # record whether the config symbols are visible
  193. # and make a list of menu nodes (that are not symbols)
  194. def handle_node(node):
  195. sym = node.item
  196. try:
  197. visible = (sym.visibility != 0)
  198. result[node] = visible
  199. except AttributeError:
  200. menus.append(node)
  201. config.walk_menu(handle_node)
  202. # now, figure out visibility for each menu. A menu is visible if any of its children are visible
  203. for m in reversed(menus): # reverse to start at leaf nodes
  204. result[m] = any(v for (n,v) in result.items() if n.parent == m)
  205. # return a dict mapping the node ID to its visibility.
  206. result = dict((confgen.get_menu_node_id(n),v) for (n,v) in result.items())
  207. return result
  208. if __name__ == '__main__':
  209. try:
  210. main()
  211. except FatalError as e:
  212. print("A fatal error occurred: %s" % e, file=sys.stderr)
  213. sys.exit(2)