| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- #!/usr/bin/env python
- #
- # Long-running server process uses stdin & stdout to communicate JSON
- # with a caller
- #
- from __future__ import print_function
- import argparse
- import confgen
- import json
- import os
- import sys
- import tempfile
- from confgen import FatalError, __version__
- try:
- from . import kconfiglib
- except Exception:
- sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
- import kconfiglib
- # Min/Max supported protocol versions
- MIN_PROTOCOL_VERSION = 1
- MAX_PROTOCOL_VERSION = 2
- def main():
- parser = argparse.ArgumentParser(description='confserver.py v%s - Config Generation Tool' % __version__, prog=os.path.basename(sys.argv[0]))
- parser.add_argument('--config',
- help='Project configuration settings',
- required=True)
- parser.add_argument('--kconfig',
- help='KConfig file with config item definitions',
- required=True)
- parser.add_argument('--sdkconfig-rename',
- help='File with deprecated Kconfig options',
- required=False)
- parser.add_argument('--env', action='append', default=[],
- help='Environment to set when evaluating the config file', metavar='NAME=VAL')
- parser.add_argument('--env-file', type=argparse.FileType('r'),
- help='Optional file to load environment variables from. Contents '
- 'should be a JSON object where each key/value pair is a variable.')
- parser.add_argument('--version', help='Set protocol version to use on initial status',
- type=int, default=MAX_PROTOCOL_VERSION)
- args = parser.parse_args()
- if args.version < MIN_PROTOCOL_VERSION:
- print("Version %d is older than minimum supported protocol version %d. Client is much older than ESP-IDF version?" %
- (args.version, MIN_PROTOCOL_VERSION))
- if args.version > MAX_PROTOCOL_VERSION:
- print("Version %d is newer than maximum supported protocol version %d. Client is newer than ESP-IDF version?" %
- (args.version, MAX_PROTOCOL_VERSION))
- try:
- args.env = [(name,value) for (name,value) in (e.split("=",1) for e in args.env)]
- except ValueError:
- print("--env arguments must each contain =. To unset an environment variable, use 'ENV='")
- sys.exit(1)
- for name, value in args.env:
- os.environ[name] = value
- if args.env_file is not None:
- env = json.load(args.env_file)
- os.environ.update(confgen.dict_enc_for_env(env))
- run_server(args.kconfig, args.config, args.sdkconfig_rename)
- def run_server(kconfig, sdkconfig, sdkconfig_rename, default_version=MAX_PROTOCOL_VERSION):
- config = kconfiglib.Kconfig(kconfig)
- sdkconfig_renames = [sdkconfig_rename] if sdkconfig_rename else []
- sdkconfig_renames += os.environ.get("COMPONENT_SDKCONFIG_RENAMES", "").split()
- deprecated_options = confgen.DeprecatedOptions(config.config_prefix, path_rename_files=sdkconfig_renames)
- f_o = tempfile.NamedTemporaryFile(mode='w+b', delete=False)
- try:
- with open(sdkconfig, mode='rb') as f_i:
- f_o.write(f_i.read())
- f_o.close() # need to close as DeprecatedOptions will reopen, and Windows only allows one open file
- deprecated_options.replace(sdkconfig_in=f_o.name, sdkconfig_out=sdkconfig)
- finally:
- os.unlink(f_o.name)
- config.load_config(sdkconfig)
- print("Server running, waiting for requests on stdin...", file=sys.stderr)
- config_dict = confgen.get_json_values(config)
- ranges_dict = get_ranges(config)
- visible_dict = get_visible(config)
- if default_version == 1:
- # V1: no 'visibility' key, send value None for any invisible item
- values_dict = dict((k, v if visible_dict[k] else False) for (k,v) in config_dict.items())
- json.dump({"version": 1, "values": values_dict, "ranges": ranges_dict}, sys.stdout)
- else:
- # V2 onwards: separate visibility from version
- json.dump({"version": default_version, "values": config_dict, "ranges": ranges_dict, "visible": visible_dict}, sys.stdout)
- print("\n")
- sys.stdout.flush()
- while True:
- line = sys.stdin.readline()
- if not line:
- break
- try:
- req = json.loads(line)
- except ValueError as e: # json module throws JSONDecodeError (sublcass of ValueError) on Py3 but ValueError on Py2
- response = {"version": default_version, "error": ["JSON formatting error: %s" % e]}
- json.dump(response, sys.stdout)
- print("\n")
- sys.stdout.flush()
- continue
- before = confgen.get_json_values(config)
- before_ranges = get_ranges(config)
- before_visible = get_visible(config)
- if "load" in req: # load a new sdkconfig
- if req.get("version", default_version) == 1:
- # for V1 protocol, send all items when loading new sdkconfig.
- # (V2+ will only send changes, same as when setting an item)
- before = {}
- before_ranges = {}
- before_visible = {}
- # if no new filename is supplied, use existing sdkconfig path, otherwise update the path
- if req["load"] is None:
- req["load"] = sdkconfig
- else:
- sdkconfig = req["load"]
- if "save" in req:
- if req["save"] is None:
- req["save"] = sdkconfig
- else:
- sdkconfig = req["save"]
- error = handle_request(deprecated_options, config, req)
- after = confgen.get_json_values(config)
- after_ranges = get_ranges(config)
- after_visible = get_visible(config)
- values_diff = diff(before, after)
- ranges_diff = diff(before_ranges, after_ranges)
- visible_diff = diff(before_visible, after_visible)
- if req["version"] == 1:
- # V1 response, invisible items have value None
- for k in (k for (k,v) in visible_diff.items() if not v):
- values_diff[k] = None
- response = {"version": 1, "values": values_diff, "ranges": ranges_diff}
- else:
- # V2+ response, separate visibility values
- response = {"version": req["version"], "values": values_diff, "ranges": ranges_diff, "visible": visible_diff}
- if error:
- for e in error:
- print("Error: %s" % e, file=sys.stderr)
- response["error"] = error
- json.dump(response, sys.stdout)
- print("\n")
- sys.stdout.flush()
- def handle_request(deprecated_options, config, req):
- if "version" not in req:
- return ["All requests must have a 'version'"]
- if req["version"] < MIN_PROTOCOL_VERSION or req["version"] > MAX_PROTOCOL_VERSION:
- return ["Unsupported request version %d. Server supports versions %d-%d" % (
- req["version"],
- MIN_PROTOCOL_VERSION,
- MAX_PROTOCOL_VERSION)]
- error = []
- if "load" in req:
- print("Loading config from %s..." % req["load"], file=sys.stderr)
- try:
- config.load_config(req["load"])
- except Exception as e:
- error += ["Failed to load from %s: %s" % (req["load"], e)]
- if "set" in req:
- handle_set(config, error, req["set"])
- if "save" in req:
- try:
- print("Saving config to %s..." % req["save"], file=sys.stderr)
- confgen.write_config(deprecated_options, config, req["save"])
- except Exception as e:
- error += ["Failed to save to %s: %s" % (req["save"], e)]
- return error
- def handle_set(config, error, to_set):
- missing = [k for k in to_set if k not in config.syms]
- if missing:
- error.append("The following config symbol(s) were not found: %s" % (", ".join(missing)))
- # replace name keys with the full config symbol for each key:
- to_set = dict((config.syms[k],v) for (k,v) in to_set.items() if k not in missing)
- # Work through the list of values to set, noting that
- # some may not be immediately applicable (maybe they depend
- # on another value which is being set). Therefore, defer
- # knowing if any value is unsettable until then end
- while len(to_set):
- set_pass = [(k,v) for (k,v) in to_set.items() if k.visibility]
- if not set_pass:
- break # no visible keys left
- for (sym,val) in set_pass:
- if sym.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
- if val is True:
- sym.set_value(2)
- elif val is False:
- sym.set_value(0)
- else:
- error.append("Boolean symbol %s only accepts true/false values" % sym.name)
- elif sym.type == kconfiglib.HEX:
- try:
- if not isinstance(val, int):
- val = int(val, 16) # input can be a decimal JSON value or a string of hex digits
- sym.set_value(hex(val))
- except ValueError:
- error.append("Hex symbol %s can accept a decimal integer or a string of hex digits, only")
- else:
- sym.set_value(str(val))
- print("Set %s" % sym.name)
- del to_set[sym]
- if len(to_set):
- error.append("The following config symbol(s) were not visible so were not updated: %s" % (", ".join(s.name for s in to_set)))
- def diff(before, after):
- """
- Return a dictionary with the difference between 'before' and 'after',
- for items which are present in 'after' dictionary
- """
- diff = dict((k,v) for (k,v) in after.items() if before.get(k, None) != v)
- return diff
- def get_ranges(config):
- ranges_dict = {}
- def is_base_n(i, n):
- try:
- int(i, n)
- return True
- except ValueError:
- return False
- def get_active_range(sym):
- """
- Returns a tuple of (low, high) integer values if a range
- limit is active for this symbol, or (None, None) if no range
- limit exists.
- """
- base = kconfiglib._TYPE_TO_BASE[sym.orig_type] if sym.orig_type in kconfiglib._TYPE_TO_BASE else 0
- try:
- for low_expr, high_expr, cond in sym.ranges:
- if kconfiglib.expr_value(cond):
- low = int(low_expr.str_value, base) if is_base_n(low_expr.str_value, base) else 0
- high = int(high_expr.str_value, base) if is_base_n(high_expr.str_value, base) else 0
- return (low, high)
- except ValueError:
- pass
- return (None, None)
- def handle_node(node):
- sym = node.item
- if not isinstance(sym, kconfiglib.Symbol):
- return
- active_range = get_active_range(sym)
- if active_range[0] is not None:
- ranges_dict[sym.name] = active_range
- for n in config.node_iter():
- handle_node(n)
- return ranges_dict
- def get_visible(config):
- """
- Return a dict mapping node IDs (config names or menu node IDs) to True/False for their visibility
- """
- result = {}
- menus = []
- # when walking the menu the first time, only
- # record whether the config symbols are visible
- # and make a list of menu nodes (that are not symbols)
- def handle_node(node):
- sym = node.item
- try:
- visible = (sym.visibility != 0)
- result[node] = visible
- except AttributeError:
- menus.append(node)
- for n in config.node_iter():
- handle_node(n)
- # now, figure out visibility for each menu. A menu is visible if any of its children are visible
- for m in reversed(menus): # reverse to start at leaf nodes
- result[m] = any(v for (n,v) in result.items() if n.parent == m)
- # return a dict mapping the node ID to its visibility.
- result = dict((confgen.get_menu_node_id(n),v) for (n,v) in result.items())
- return result
- if __name__ == '__main__':
- try:
- main()
- except FatalError as e:
- print("A fatal error occurred: %s" % e, file=sys.stderr)
- sys.exit(2)
|