kemidocs.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. # tool to generate the modules.md content
  2. #
  3. KAMWEBURL = "https://kamailio.org"
  4. MODMAPNAME = {
  5. "pvx": "pv",
  6. "kx": "kemi",
  7. }
  8. MODMAPFUNC = {
  9. "alias_db": {
  10. "lookup": "alias_db_lookup",
  11. "lookup_ex": "alias_db_lookup",
  12. },
  13. "app_jsdt": {
  14. "dofile": "jsdt_dofile",
  15. "dostring": "jsdt_dostring",
  16. "run": "jsdt_run",
  17. },
  18. }
  19. import os, json, sys, time, fnmatch, re, importlib
  20. class ModuleDocGenerator(object):
  21. PATH_GENERATED_MARKDOWN = "../docs/modules.md"
  22. PATH_MODULES_DOCS = "../docs/modules/"
  23. # Contains the output until it should be written to disk
  24. markdown_string = ""
  25. def execute(self, data):
  26. # Validate that we got some methods back. 155 is an arbitrary large number.
  27. if len(data) < 1:
  28. print("ERR: Invalid data")
  29. exit()
  30. functions_parsed = self.parse_function_list(data)
  31. self.output_markdown(functions_parsed)
  32. print ("Markdown doc created successfully at " + self.PATH_GENERATED_MARKDOWN)
  33. def parse_function_list(self, functions):
  34. data = {}
  35. for elem in functions:
  36. module = elem["module"]
  37. # TODO: What about the hdr, pv, x sub-module?
  38. if module == "":
  39. module = "core"
  40. if module not in data:
  41. data[module] = []
  42. data[module].append({"name": elem["name"], "return": elem["ret"], "params": elem["params"]})
  43. return data
  44. def output_markdown(self, data):
  45. self.markdown_header()
  46. for key in sorted(data):
  47. methods = data[key]
  48. # Sort the functions by name alphabetically
  49. methods = sorted(methods, key = lambda k: k['name'])
  50. self.markdown_section_heading(key)
  51. self.markdown_section_content(key, methods)
  52. self.markdown_write()
  53. return True
  54. def markdown_header(self):
  55. self.markdown_string += self.read_file_to_string("header.md")
  56. return True
  57. def markdown_section_heading(self, module):
  58. self.markdown_string += "## " + module + " ##\n\n"
  59. kmodname = MODMAPNAME[module] if module in MODMAPNAME else module
  60. self.markdown_string += " * <a target='_blank' href='" + KAMWEBURL + "/docs/modules/devel/modules/" \
  61. + kmodname + ".html'>📖 kamailio.cfg::module::" + kmodname + ".html</a>\n\n"
  62. self.markdown_string += self.read_file_to_string(module + "/" + module + ".header.md")
  63. return True
  64. def markdown_section_content(self, module, methods):
  65. if module == "core":
  66. module_prefix = ""
  67. else:
  68. module_prefix = module + "."
  69. kmodtoc = "Exported functions:\n\n"
  70. kmodtext = ""
  71. kmodname = MODMAPNAME[module] if module in MODMAPNAME else module
  72. for value in methods:
  73. kmodtoc += " * [KSR." + module_prefix + value["name"] + "()](#ksr" + module_prefix.replace('.', '') + value["name"] + ")\n"
  74. kmodtext += "#### KSR." + module_prefix + value["name"] + "() ####\n\n"
  75. # Sanitize the return values
  76. if value["return"] == "none":
  77. return_value = "void"
  78. else:
  79. return_value = value["return"]
  80. # Sanitize the parameter values
  81. if value["params"] == "none":
  82. params_value = ""
  83. else:
  84. params_value = value["params"]
  85. # Generate the output string for the markdown page
  86. kmodtext += "```cpp\n" + return_value + " KSR." + module_prefix + value["name"] \
  87. + "(" + params_value + ");\n```\n\n" \
  88. kfuncname = value["name"]
  89. if kmodname in MODMAPFUNC:
  90. if value["name"] in MODMAPFUNC[kmodname]:
  91. kfuncname = MODMAPFUNC[kmodname][value["name"]]
  92. kmodtext += " * <a target='_blank' href='" + KAMWEBURL + "/docs/modules/devel/modules/" + kmodname + ".html#" \
  93. + kmodname + ".f." + kfuncname + "'>📖 kamailio.cfg::function::" + kfuncname + "()</a>\n\n"
  94. func_doc = self.read_file_to_string(module + "/" + module + "." + value["name"] + ".md").strip()
  95. if len(func_doc)>0:
  96. kmodtext += func_doc + "\n\n"
  97. self.markdown_string += kmodtoc + "\n" + kmodtext
  98. return True
  99. def markdown_write(self):
  100. f = open(self.PATH_GENERATED_MARKDOWN, "w")
  101. f.write(self.markdown_string)
  102. f.close()
  103. return True
  104. def read_file_to_string(self, filename):
  105. path = self.PATH_MODULES_DOCS + filename
  106. if os.path.isfile(path):
  107. with open(path, 'r') as myfile:
  108. return myfile.read() + "\n"
  109. return ""
  110. class KemiFileExportParser(object):
  111. # These functions are created by a macro so makes the parsing somewhat tricky,
  112. # for now they are statically defined
  113. macro_functions = {
  114. "t_set_auto_inv_100": "int state",
  115. "t_set_disable_6xx": "int state",
  116. "t_set_disable_failover": "int state",
  117. "t_set_no_e2e_cancel_reason": "int state",
  118. "t_set_disable_internal_reply": "int state"
  119. }
  120. # These files export the KEMI functions in a special way so we map them manually
  121. # TODO: Discuss with @miconda if core/HDR/pv/x should be added as well or not
  122. special_exports = [
  123. #{"filename": "kemi.c", "export": "_sr_kemi_core", "folder": "/core/"},
  124. #{"filename": "kemi.c", "export": "_sr_kemi_hdr", "folder": "/core/"},
  125. #{"filename": "app_lua_mod.c", "export": "sr_kemi_app_lua_rpc_exports", "folder": "/modules/app_lua/"}
  126. ]
  127. def generate_kemi_export_list(self, source_path):
  128. files = self.list_c_files_in_directory(source_path)
  129. lists = []
  130. for file in files:
  131. with open(file, 'r', encoding='utf-8', errors='ignore') as f:
  132. lines = f.readlines()
  133. export_name = self.find_c_file_kemi_export(file, lines)
  134. if export_name:
  135. export_functions = self.extract_c_file_kemi_export_lines(file, lines, export_name)
  136. lists = lists + export_functions
  137. print ("Found ", len(export_functions), "functions", "Total:", len(lists))
  138. # Handle some special files separately
  139. for elem in self.special_exports:
  140. file = source_path + elem["folder"] + elem["filename"]
  141. with open(file) as f:
  142. lines = f.readlines()
  143. lists = lists + self.extract_c_file_kemi_export_lines(file, lines, elem["export"])
  144. return lists
  145. def find_c_file_kemi_export(self, filename, lines):
  146. param = None
  147. for line in lines:
  148. if line.find("sr_kemi_modules_add") >= 0:
  149. line = line.lstrip(" ")
  150. line = line.lstrip("\t")
  151. if line.find("sr_kemi_modules_add") == 0:
  152. param = line[line.find("(") + 1:line.find(")")]
  153. print ("INFO: ---- Found export", filename, param)
  154. break
  155. else:
  156. if line != "int sr_kemi_modules_add(sr_kemi_t *klist)\n":
  157. print ("ERR: Possible error at line: ", filename, line)
  158. exit()
  159. return param
  160. def extract_c_file_kemi_export_lines(self, filename, lines, export_name):
  161. list_functions = []
  162. find_start = True
  163. for line in lines:
  164. if find_start and line.find("static sr_kemi_t " + export_name + "[]") >= 0:
  165. find_start = False
  166. elif not find_start:
  167. if line.find("};") >= 0:
  168. break
  169. line = line.lstrip(" \t")
  170. line = line.rstrip()
  171. list_functions.append(line)
  172. if len(list_functions) < 1:
  173. print ("ERR: Couldn't parse file for exported functions: ", export_name)
  174. exit()
  175. parsed_list = self.parse_kemi_export_c_lines(filename, list_functions)
  176. return parsed_list
  177. def parse_kemi_export_c_lines(self, filename, lines):
  178. elements = []
  179. function_lines = []
  180. temp_function = ""
  181. # We look for str_init which would be the start of each exported function
  182. for line in lines:
  183. if line.find("str_init") >= 0:
  184. if temp_function != "":
  185. function_lines.append(temp_function)
  186. temp_function = ""
  187. temp_function += line
  188. if temp_function != "":
  189. function_lines.append(temp_function)
  190. # Now we parse each exported function to extract its declaration
  191. for func in function_lines:
  192. function_lines_split = func.split(",{")
  193. if len(function_lines_split) < 2:
  194. print ("ERR: Incorrect function line", func)
  195. exit()
  196. declarations = function_lines_split[0].split(",")
  197. params = function_lines_split[1]
  198. # Extract the content from the definitions
  199. val_module = declarations[0]
  200. val_module = val_module[val_module.find('("') + 2:val_module.find('")')]
  201. val_function = declarations[1]
  202. val_function = val_function[val_function.find('("') + 2:val_function.find('")')]
  203. if declarations[2] == "SR_KEMIP_INT":
  204. val_return = "int"
  205. elif declarations[2] == "SR_KEMIP_STR":
  206. val_return = "string"
  207. elif declarations[2] == "SR_KEMIP_NONE":
  208. val_return = "void"
  209. elif declarations[2] == "SR_KEMIP_BOOL":
  210. val_return = "bool"
  211. elif declarations[2] == "SR_KEMIP_XVAL":
  212. val_return = "xval"
  213. else:
  214. print("ERR: Invalid return value", declarations[2], func)
  215. exit()
  216. val_c_function = declarations[3].strip()
  217. # Count how many parameters the KEMI C function expects
  218. val_params = []
  219. itr = 0
  220. for val in params.rstrip("},").split(","):
  221. itr += 1
  222. # KEMI function has a maximum of 6 params
  223. if itr > 6:
  224. break
  225. pm = val.strip()
  226. if pm == "SR_KEMIP_INT":
  227. val_params.append("int")
  228. elif pm == "SR_KEMIP_STR":
  229. val_params.append("str")
  230. elif pm == "SR_KEMIP_NONE":
  231. continue
  232. else:
  233. print("Invalid return value", declarations[2], func)
  234. exit()
  235. if itr != 6:
  236. print("ERR: Couldn't iterate the params: ", params)
  237. exit()
  238. param_string = self.find_c_function_params(filename, val_c_function, val_return)
  239. param_string = self.prettify_params_list(val_function, param_string, val_params)
  240. elements.append({"module": val_module, "name": val_function, "ret": val_return, "params": param_string})
  241. return elements
  242. def prettify_params_list(self, function_name, function_declaration, kemi_types):
  243. # Validate the quantity and types of declaration vs export
  244. if function_declaration == "" and len(kemi_types) == 0:
  245. return ""
  246. params = function_declaration.split(",")
  247. if params[0].find("sip_msg_t") >= 0 or params[0].find("struct sip_msg") >= 0:
  248. params.pop(0)
  249. if len(params) != len(kemi_types):
  250. print("ERR: Mismatching quantity of params. Declaration for", function_name, ":", function_declaration, "KEMI:", kemi_types)
  251. exit()
  252. for declared, type in zip(params, kemi_types):
  253. declared = declared.replace("*", "")
  254. declared = declared.strip().split(" ")[0]
  255. if declared != type:
  256. print("ERR: Mismatching type of params for", function_name, ":", function_declaration, " | ", kemi_types, " | Declared: ", declared, " Type: ", type)
  257. exit()
  258. param_string = ""
  259. for param in params:
  260. param = param.strip()
  261. param = param.replace("*", "")
  262. if param[:3] == "str":
  263. temp = param.split(" ")
  264. param = "str" + ' "' + temp[1] + '"'
  265. param_string += param + ", "
  266. # Clean up the presentation of the params
  267. param_string = param_string.rstrip(", ")
  268. return param_string
  269. def find_c_function_params(self, filename, function_name, return_type):
  270. # First we try with the same file to find the declaration
  271. param_string = self.search_file_for_function_declaration(filename, function_name, return_type)
  272. # If we couldn't find it, let's try all files in the same folder as the first file
  273. if param_string:
  274. return param_string
  275. else:
  276. files = self.list_c_files_in_directory(os.path.dirname(filename))
  277. for file in files:
  278. param_string = self.search_file_for_function_declaration(file, function_name, return_type)
  279. if param_string:
  280. return param_string
  281. if function_name in self.macro_functions:
  282. return self.macro_functions[function_name]
  283. print("ERR: Couldn't find the function declaration", filename, function_name, return_type)
  284. exit()
  285. def search_file_for_function_declaration(self, filename, function_name, return_type):
  286. # print "Searching file", filename, "for", function_name
  287. with open(filename, 'r', encoding='utf-8', errors='ignore') as f:
  288. lines = f.readlines()
  289. param_string = None
  290. found = False
  291. temp_string = ""
  292. return_match = return_type
  293. # KEMI has some magic where most functions actually return INTs but KEMI maps them to void/bool
  294. if return_type == "void" or return_type == "bool":
  295. return_match = "int"
  296. if return_type == "xval":
  297. return_match = "sr_kemi_xval_t([ \t])*\*"
  298. # Look for declarations in format: static? return_type function_name(
  299. r = re.compile("^(?:static )?" + return_match + "[ \t]*(" + function_name + ")[ \t]*\(")
  300. for line in lines:
  301. m = r.match(line)
  302. if m:
  303. found = True
  304. if found:
  305. temp_string += line
  306. if line.find("{") >= 0:
  307. param_string = temp_string[temp_string.find('(') + 1:temp_string.find(')')]
  308. break
  309. return param_string
  310. def list_c_files_in_directory(self, path):
  311. matches = []
  312. for root, dirnames, filenames in os.walk(path):
  313. for filename in fnmatch.filter(filenames, '*.c'):
  314. matches.append(os.path.join(root, filename))
  315. return matches
  316. if __name__ == "__main__":
  317. try:
  318. if not os.path.isdir(sys.argv[1]):
  319. raise Exception('Not a valid directory')
  320. except:
  321. print("Please provide the path to the Kamailio src folder as the first argument")
  322. exit()
  323. print("Parsing the source")
  324. parser = KemiFileExportParser()
  325. data = parser.generate_kemi_export_list(sys.argv[1].rstrip("/"))
  326. fgen = ModuleDocGenerator()
  327. fgen.execute(data)
  328. print("Done")