1#!/usr/bin/env python3
2
3# Copyright 2023 gRPC authors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17from __future__ import print_function
18
19import binascii
20import collections
21import ctypes
22import datetime
23import json
24import math
25import os
26import re
27import sys
28
29import yaml
30
31with open('src/core/lib/config/config_vars.yaml') as f:
32    attrs = yaml.safe_load(f.read(), Loader=yaml.FullLoader)
33
34error = False
35today = datetime.date.today()
36two_quarters_from_now = today + datetime.timedelta(days=180)
37for attr in attrs:
38    if 'name' not in attr:
39        print("config has no name: %r" % attr)
40        error = True
41        continue
42    if 'experiment' in attr['name'] and attr['name'] != 'experiments':
43        print('use experiment system for experiments')
44        error = True
45    if 'description' not in attr:
46        print("no description for %s" % attr['name'])
47        error = True
48    if 'default' not in attr:
49        print("no default for %s" % attr['name'])
50        error = True
51
52if error:
53    sys.exit(1)
54
55
56def c_str(s, encoding='ascii'):
57    if s is None:
58        return '""'
59    if isinstance(s, str):
60        s = s.encode(encoding)
61    result = ''
62    for c in s:
63        c = chr(c) if isinstance(c, int) else c
64        if not (32 <= ord(c) < 127) or c in ('\\', '"'):
65            result += '\\%03o' % ord(c)
66        else:
67            result += c
68    return '"' + result + '"'
69
70
71def snake_to_pascal(s):
72    return ''.join(x.capitalize() for x in s.split('_'))
73
74
75# utility: print a big comment block into a set of files
76def put_banner(files, banner):
77    for f in files:
78        for line in banner:
79            print('// %s' % line, file=f)
80        print(file=f)
81
82
83def put_copyright(file):
84    # copy-paste copyright notice from this file
85    with open(sys.argv[0]) as my_source:
86        copyright = []
87        for line in my_source:
88            if line[0] != '#':
89                break
90        for line in my_source:
91            if line[0] == '#':
92                copyright.append(line)
93                break
94        for line in my_source:
95            if line[0] != '#':
96                break
97            copyright.append(line)
98        put_banner([file], [line[2:].rstrip() for line in copyright])
99
100
101RETURN_TYPE = {
102    "int": "int32_t",
103    "string": "absl::string_view",
104    "comma_separated_string": "absl::string_view",
105    "bool": "bool",
106}
107
108MEMBER_TYPE = {
109    "int": "int32_t",
110    "string": "std::string",
111    "comma_separated_string": "std::string",
112    "bool": "bool",
113}
114
115FLAG_TYPE = {
116    "int": "absl::optional<int32_t>",
117    "string": "absl::optional<std::string>",
118    "comma_separated_string": "std::vector<std::string>",
119    "bool": "absl::optional<bool>",
120}
121
122PROTO_TYPE = {
123    "int": "int32",
124    "string": "string",
125    "comma_separated_string": "string",
126    "bool": "bool",
127}
128
129SORT_ORDER_FOR_PACKING = {
130    "int": 0,
131    "bool": 1,
132    "string": 2,
133    "comma_separated_string": 3
134}
135
136
137def bool_default_value(x, name):
138    if x == True:
139        return "true"
140    if x == False:
141        return "false"
142    if isinstance(x, str) and x.startswith("$"):
143        return x[1:]
144    return x
145
146
147def int_default_value(x, name):
148    if isinstance(x, str) and x.startswith("$"):
149        return x[1:]
150    return x
151
152
153def string_default_value(x, name):
154    if x is None:
155        return "\"\""
156    if x.startswith("$"):
157        return x[1:]
158    return c_str(x)
159
160
161DEFAULT_VALUE = {
162    "int": int_default_value,
163    "bool": bool_default_value,
164    "string": string_default_value,
165    "comma_separated_string": string_default_value,
166}
167
168TO_STRING = {
169    "int": "$",
170    "bool": "$?\"true\":\"false\"",
171    "string": "\"\\\"\", absl::CEscape($), \"\\\"\"",
172    "comma_separated_string": "\"\\\"\", absl::CEscape($), \"\\\"\"",
173}
174
175attrs_in_packing_order = sorted(attrs,
176                                key=lambda a: SORT_ORDER_FOR_PACKING[a['type']])
177
178with open('test/core/util/fuzz_config_vars.proto', 'w') as P:
179    put_copyright(P)
180
181    put_banner([P], [
182        "", "Automatically generated by tools/codegen/core/gen_config_vars.py",
183        ""
184    ])
185
186    print("syntax = \"proto3\";", file=P)
187    print(file=P)
188    print("package grpc.testing;", file=P)
189    print(file=P)
190    print("message FuzzConfigVars {", file=P)
191    for attr in attrs_in_packing_order:
192        if attr.get("fuzz", False) == False:
193            continue
194        print("  optional %s %s = %d;" %
195              (PROTO_TYPE[attr['type']], attr['name'],
196               binascii.crc32(attr['name'].encode('ascii')) & 0x1FFFFFFF),
197              file=P)
198    print("};", file=P)
199
200with open('test/core/util/fuzz_config_vars.h', 'w') as H:
201    put_copyright(H)
202
203    put_banner([H], [
204        "", "Automatically generated by tools/codegen/core/gen_config_vars.py",
205        ""
206    ])
207
208    print("#ifndef GRPC_TEST_CORE_UTIL_FUZZ_CONFIG_VARS_H", file=H)
209    print("#define GRPC_TEST_CORE_UTIL_FUZZ_CONFIG_VARS_H", file=H)
210    print(file=H)
211    print("#include <grpc/support/port_platform.h>", file=H)
212    print(file=H)
213    print("#include \"test/core/util/fuzz_config_vars.pb.h\"", file=H)
214    print("#include \"src/core/lib/config/config_vars.h\"", file=H)
215    print(file=H)
216    print("namespace grpc_core {", file=H)
217    print(file=H)
218    print(
219        "ConfigVars::Overrides OverridesFromFuzzConfigVars(const grpc::testing::FuzzConfigVars& vars);",
220        file=H)
221    print(
222        "void ApplyFuzzConfigVars(const grpc::testing::FuzzConfigVars& vars);",
223        file=H)
224    print(file=H)
225    print("}  // namespace grpc_core", file=H)
226    print(file=H)
227    print("#endif  // GRPC_TEST_CORE_UTIL_FUZZ_CONFIG_VARS_H", file=H)
228
229with open('test/core/util/fuzz_config_vars.cc', 'w') as C:
230    put_copyright(C)
231
232    put_banner([C], [
233        "", "Automatically generated by tools/codegen/core/gen_config_vars.py",
234        ""
235    ])
236
237    print("#include \"test/core/util/fuzz_config_vars.h\"", file=C)
238    print("#include \"test/core/util/fuzz_config_vars_helpers.h\"", file=C)
239    print(file=C)
240    print("namespace grpc_core {", file=C)
241    print(file=C)
242    print(
243        "ConfigVars::Overrides OverridesFromFuzzConfigVars(const grpc::testing::FuzzConfigVars& vars) {",
244        file=C)
245    print("  ConfigVars::Overrides overrides;", file=C)
246    for attr in attrs_in_packing_order:
247        fuzz = attr.get("fuzz", False)
248        if not fuzz:
249            continue
250        print("  if (vars.has_%s()) {" % attr['name'], file=C)
251        if isinstance(fuzz, str):
252            print("    overrides.%s = %s(vars.%s());" %
253                  (attr['name'], fuzz, attr['name']),
254                  file=C)
255        else:
256            print("    overrides.%s = vars.%s();" %
257                  (attr['name'], attr['name']),
258                  file=C)
259        print("  }", file=C)
260    print("  return overrides;", file=C)
261    print("}", file=C)
262    print(
263        "void ApplyFuzzConfigVars(const grpc::testing::FuzzConfigVars& vars) {",
264        file=C)
265    print("  ConfigVars::SetOverrides(OverridesFromFuzzConfigVars(vars));",
266          file=C)
267    print("}", file=C)
268    print(file=C)
269    print("}  // namespace grpc_core", file=C)
270
271with open('src/core/lib/config/config_vars.h', 'w') as H:
272    put_copyright(H)
273
274    put_banner([H], [
275        "", "Automatically generated by tools/codegen/core/gen_config_vars.py",
276        ""
277    ])
278
279    print("#ifndef GRPC_SRC_CORE_LIB_CONFIG_CONFIG_VARS_H", file=H)
280    print("#define GRPC_SRC_CORE_LIB_CONFIG_CONFIG_VARS_H", file=H)
281    print(file=H)
282    print("#include <grpc/support/port_platform.h>", file=H)
283    print(file=H)
284    print("#include <string>", file=H)
285    print("#include <atomic>", file=H)
286    print("#include <stdint.h>", file=H)
287    print("#include \"absl/strings/string_view.h\"", file=H)
288    print("#include \"absl/types/optional.h\"", file=H)
289    print(file=H)
290    print("namespace grpc_core {", file=H)
291    print(file=H)
292    print("class ConfigVars {", file=H)
293    print(" public:", file=H)
294    print("  struct Overrides {", file=H)
295    for attr in attrs_in_packing_order:
296        print("    absl::optional<%s> %s;" %
297              (MEMBER_TYPE[attr['type']], attr['name']),
298              file=H)
299    print("  };", file=H)
300    print("  ConfigVars(const ConfigVars&) = delete;", file=H)
301    print("  ConfigVars& operator=(const ConfigVars&) = delete;", file=H)
302    print("  // Get the core configuration; if it does not exist, create it.",
303          file=H)
304    print("  static const ConfigVars& Get() {", file=H)
305    print("    auto* p = config_vars_.load(std::memory_order_acquire);", file=H)
306    print("    if (p != nullptr) return *p;", file=H)
307    print("    return Load();", file=H)
308    print("  }", file=H)
309    print("  static void SetOverrides(const Overrides& overrides);", file=H)
310    print("  // Drop the config vars. Users must ensure no other threads are",
311          file=H)
312    print("  // accessing the configuration.", file=H)
313    print("  static void Reset();", file=H)
314    print("  std::string ToString() const;", file=H)
315    for attr in attrs:
316        for line in attr['description'].splitlines():
317            print("  // %s" % line, file=H)
318        if attr.get('force-load-on-access', False):
319            print("  %s %s() const;" %
320                  (MEMBER_TYPE[attr['type']], snake_to_pascal(attr['name'])),
321                  file=H)
322        else:
323            print("  %s %s() const { return %s_; }" %
324                  (RETURN_TYPE[attr['type']], snake_to_pascal(
325                      attr['name']), attr['name']),
326                  file=H)
327    print(" private:", file=H)
328    print("  explicit ConfigVars(const Overrides& overrides);", file=H)
329    print("  static const ConfigVars& Load();", file=H)
330    print("  static std::atomic<ConfigVars*> config_vars_;", file=H)
331    for attr in attrs_in_packing_order:
332        if attr.get('force-load-on-access', False):
333            continue
334        print("  %s %s_;" % (MEMBER_TYPE[attr['type']], attr['name']), file=H)
335    for attr in attrs_in_packing_order:
336        if attr.get('force-load-on-access', False) == False:
337            continue
338        print("  absl::optional<%s> override_%s_;" %
339              (MEMBER_TYPE[attr['type']], attr['name']),
340              file=H)
341    print("};", file=H)
342    print(file=H)
343    print("}  // namespace grpc_core", file=H)
344    print(file=H)
345    print("#endif  // GRPC_SRC_CORE_LIB_CONFIG_CONFIG_VARS_H", file=H)
346
347with open('src/core/lib/config/config_vars.cc', 'w') as C:
348    put_copyright(C)
349
350    put_banner([C], [
351        "", "Automatically generated by tools/codegen/core/gen_config_vars.py",
352        ""
353    ])
354
355    print("#include <grpc/support/port_platform.h>", file=C)
356    print("#include \"src/core/lib/config/config_vars.h\"", file=C)
357    print("#include \"src/core/lib/config/load_config.h\"", file=C)
358    print("#include \"absl/strings/escaping.h\"", file=C)
359    print("#include \"absl/flags/flag.h\"", file=C)
360    print(file=C)
361
362    for attr in attrs:
363        if 'prelude' in attr:
364            print(attr['prelude'], file=C)
365
366    for attr in attrs:
367        print("ABSL_FLAG(%s, %s, {}, %s);" %
368              (FLAG_TYPE[attr["type"]], 'grpc_' + attr['name'],
369               c_str(attr['description'])),
370              file=C)
371    print(file=C)
372    print("namespace grpc_core {", file=C)
373    print(file=C)
374    print("ConfigVars::ConfigVars(const Overrides& overrides) :", file=C)
375    initializers = [
376        "%s_(LoadConfig(FLAGS_grpc_%s, \"GRPC_%s\", overrides.%s, %s))" %
377        (attr['name'], attr['name'], attr['name'].upper(), attr['name'],
378         DEFAULT_VALUE[attr['type']](attr['default'], attr['name']))
379        for attr in attrs_in_packing_order
380        if attr.get('force-load-on-access', False) == False
381    ]
382    initializers += [
383        "override_%s_(overrides.%s)" % (attr['name'], attr['name'])
384        for attr in attrs_in_packing_order
385        if attr.get('force-load-on-access', False)
386    ]
387    print(",".join(initializers), file=C)
388    print("{}", file=C)
389    print(file=C)
390    for attr in attrs:
391        if attr.get('force-load-on-access', False):
392            print(
393                "%s ConfigVars::%s() const { return LoadConfig(FLAGS_grpc_%s, \"GRPC_%s\", override_%s_, %s); }"
394                % (MEMBER_TYPE[attr['type']], snake_to_pascal(attr['name']),
395                   attr['name'], attr['name'].upper(), attr['name'],
396                   DEFAULT_VALUE[attr['type']](attr['default'], attr['name'])),
397                file=C)
398            print(file=C)
399    print("std::string ConfigVars::ToString() const {", file=C)
400    print("  return absl::StrCat(", file=C)
401    for i, attr in enumerate(attrs):
402        if i:
403            print(",", file=C)
404            print(c_str(", " + attr['name'] + ": "), file=C)
405        else:
406            print(c_str(attr['name'] + ": "), file=C)
407        print(",",
408              TO_STRING[attr['type']].replace(
409                  "$",
410                  snake_to_pascal(attr['name']) + "()"),
411              file=C)
412    print(");}", file=C)
413    print(file=C)
414    print("}", file=C)
415