incremental debuggung
This commit is contained in:
parent
6d8acdea9f
commit
6169ea6bed
17 changed files with 203 additions and 64 deletions
|
|
@ -502,6 +502,44 @@ from_client_load(clicon_handle h,
|
|||
return retval;
|
||||
}
|
||||
|
||||
/*! Internal message: Copy file from file1 to file2
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] s Socket where request arrived, and where replies are sent
|
||||
* @param[in] pid Unix process id
|
||||
* @param[in] msg Message
|
||||
* @param[in] label Memory chunk
|
||||
* @retval 0 OK
|
||||
* @retval -1 Error. Send error message back to client.
|
||||
*/
|
||||
static int
|
||||
from_client_copy(clicon_handle h,
|
||||
int s,
|
||||
int pid,
|
||||
struct clicon_msg *msg,
|
||||
const char *label)
|
||||
{
|
||||
char *db1;
|
||||
char *db2;
|
||||
int retval = -1;
|
||||
|
||||
if (clicon_msg_copy_decode(msg,
|
||||
&db1,
|
||||
&db2,
|
||||
label) < 0){
|
||||
send_msg_err(s, clicon_errno, clicon_suberrno,
|
||||
clicon_err_reason);
|
||||
goto done;
|
||||
}
|
||||
if (xmldb_copy(h, db1, db2) < 0)
|
||||
goto done;
|
||||
if (send_msg_ok(s) < 0)
|
||||
goto done;
|
||||
retval = 0;
|
||||
done:
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
||||
/*! Internal message: Kill session (Kill the process)
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] s Client socket where request arrived, and where replies are sent
|
||||
|
|
@ -736,6 +774,10 @@ from_client(int s, void* arg)
|
|||
if (from_client_load(h, ce->ce_s, ce->ce_pid, msg, __FUNCTION__) < 0)
|
||||
goto done;
|
||||
break;
|
||||
case CLICON_MSG_COPY:
|
||||
if (from_client_copy(h, ce->ce_s, ce->ce_pid, msg, __FUNCTION__) < 0)
|
||||
goto done;
|
||||
break;
|
||||
case CLICON_MSG_KILL:
|
||||
if (from_client_kill(h, ce->ce_s, msg, __FUNCTION__) < 0)
|
||||
goto done;
|
||||
|
|
|
|||
|
|
@ -542,13 +542,11 @@ main(int argc, char **argv)
|
|||
if (rundb_main(h, app_config_file) < 0)
|
||||
goto done;
|
||||
|
||||
/* Initiate the shared candidate. Maybe we should not do this? */
|
||||
/* Initiate the shared candidate. Maybe we should not do this?
|
||||
* Too strict access
|
||||
*/
|
||||
if (xmldb_copy(h, "running", "candidate") < 0)
|
||||
goto done;
|
||||
#ifdef OBSOLETE
|
||||
/* XXX Hack for now. Change mode so that we all can write. Security issue*/
|
||||
chmod(candidate_db, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
|
||||
#endif
|
||||
if (once)
|
||||
goto done;
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ init_candidate_db(clicon_handle h)
|
|||
goto err;
|
||||
}
|
||||
if (xmldb_exists(h, "candidate") != 1)
|
||||
if (xmldb_copy(h, "running", "candidate") < 0)
|
||||
if (clicon_rpc_copy(h, "running", "candidate") < 0)
|
||||
goto err;
|
||||
retval = 0;
|
||||
err:
|
||||
|
|
@ -648,7 +648,7 @@ compare_dbs(clicon_handle h, cvec *cvv, cg_var *arg)
|
|||
}
|
||||
|
||||
|
||||
/*! Modify xml database frm a callback using xml key format strings
|
||||
/*! Modify xml database from a callback using xml key format strings
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] cvv Vector of cli string and instantiated variables
|
||||
* @param[in] arg An xml key format string, eg /aaa/%s
|
||||
|
|
@ -676,18 +676,6 @@ cli_dbxml(clicon_handle h,
|
|||
cg_var *cval;
|
||||
char *val = NULL;
|
||||
|
||||
/*
|
||||
* clicon_rpc_xmlput(h, db, MERGE,"<interfaces><interface><name>eth0</name><type>hej</type></interface><interfaces>");
|
||||
* Wanted database content:
|
||||
* /interfaces
|
||||
* /interfaces/interface/eth0
|
||||
* /interfaces/interface/eth0/name eth0
|
||||
* /interfaces/interface/eth0/type hej
|
||||
* Algorithm alt1:
|
||||
* arg = "<interfaces><interface><name>$1</name><type>$2</type></interface><interfaces>"
|
||||
* Where is arg computed? In eg yang2cli_leaf, otherwise in yang_parse,..
|
||||
* Create string using cbuf and save that.
|
||||
*/
|
||||
xkfmt = cv_string_get(arg);
|
||||
if (xmlkeyfmt2key(xkfmt, cvv, &xk) < 0)
|
||||
goto done;
|
||||
|
|
@ -944,22 +932,21 @@ delete_all(clicon_handle h, cvec *cvv, cg_var *arg)
|
|||
clicon_err(OE_PLUGIN, 0, "No such db name: %s", dbstr);
|
||||
goto done;
|
||||
}
|
||||
if (xmldb_delete(h, dbstr) < 0)
|
||||
goto done;
|
||||
if (xmldb_init(h, dbstr) < 0)
|
||||
goto done;
|
||||
if (clicon_rpc_change(h, "candidate",
|
||||
OP_REMOVE,
|
||||
"/", "") < 0)
|
||||
goto done;
|
||||
retval = 0;
|
||||
done:
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Discard all changes in candidate and replace with running
|
||||
* Utility function used by cligen spec file
|
||||
*/
|
||||
int
|
||||
discard_changes(clicon_handle h, cvec *cvv, cg_var *arg)
|
||||
{
|
||||
return xmldb_copy(h, "running", "candidate");
|
||||
return clicon_rpc_copy(h, "running", "candidate");
|
||||
}
|
||||
|
||||
/*! Generic function for showing configurations.
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ main(int argc, char **argv)
|
|||
}
|
||||
if (dumpdb){
|
||||
/* Here db must be local file-path */
|
||||
if (xmldb_dump(stdout, db, matchkey)) {
|
||||
if (xmldb_dump_local(stdout, db, matchkey)) {
|
||||
fprintf(stderr, "Match error\n");
|
||||
goto done;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -505,7 +505,7 @@ netconf_copy_config(clicon_handle h,
|
|||
goto done;
|
||||
}
|
||||
#endif
|
||||
if (xmldb_copy(h, source, target) < 0){
|
||||
if (clicon_rpc_copy(h, source, target) < 0){
|
||||
netconf_create_rpc_error(cb_err, xorig,
|
||||
"operation-failed",
|
||||
"protocol", "error",
|
||||
|
|
@ -556,7 +556,9 @@ netconf_delete_config(clicon_handle h,
|
|||
"<bad-element>target</bad-element>");
|
||||
goto done;
|
||||
}
|
||||
if (xmldb_delete(h, target) < 0){
|
||||
if (clicon_rpc_change(h, "candidate",
|
||||
OP_REMOVE,
|
||||
"/", "") < 0){
|
||||
netconf_create_rpc_error(cb_err, xorig,
|
||||
"operation-failed",
|
||||
"protocol", "error",
|
||||
|
|
@ -750,7 +752,7 @@ netconf_discard_changes(clicon_handle h,
|
|||
{
|
||||
int retval = -1;
|
||||
|
||||
if (xmldb_copy(h, "running", "candidate") < 0){
|
||||
if (clicon_rpc_copy(h, "running", "candidate") < 0){
|
||||
netconf_create_rpc_error(cb_err, xorig,
|
||||
"operation-failed",
|
||||
"protocol", "error",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ Clixon yang routing example
|
|||
|
||||
<rpc><get-config><source><candidate/></source><filter type="xpath" select="/interfaces/interface/ipv4"/></get-config></rpc>]]>]]>
|
||||
|
||||
<rpc><validate><source><candidate/></source></validate></rpc>]]>]]>
|
||||
|
||||
3. Run as docker container
|
||||
--------------------------
|
||||
cd docker
|
||||
|
|
|
|||
|
|
@ -1,21 +1,14 @@
|
|||
# Main YANG module first parsed by parser (in CLICON_YANG_DIR). eg clicon.yang.
|
||||
|
||||
# Save values as XML in database instead of lvec:s.
|
||||
# This is optimized for yang specified applications
|
||||
# But not compatible with key-based application (eg Rost)
|
||||
CLICON_DB_XML 1
|
||||
|
||||
# Startup CLI mode. This should match the CLICON_MODE in your startup clispec file
|
||||
CLICON_CLI_MODE routing
|
||||
|
||||
# Option used to construct initial yang file:
|
||||
# <module>[@<revision>]
|
||||
# This option is only relevant if CLICON_DBSPEC_TYPE is YANG
|
||||
CLICON_YANG_MODULE_MAIN ietf-ip
|
||||
|
||||
# Option used to construct initial yang file:
|
||||
# <module>[@<revision>]
|
||||
# This option is only relevant if CLICON_DBSPEC_TYPE is YANG
|
||||
CLICON_YANG_MODULE_REVISION 2014-06-16
|
||||
|
||||
# Generate code for CLI completion of existing db symbols
|
||||
|
|
|
|||
|
|
@ -71,6 +71,10 @@ enum clicon_msg_type{
|
|||
3. string: filename to load from
|
||||
|
||||
*/
|
||||
CLICON_MSG_COPY, /* Copy from file to file in backend. Body is:
|
||||
1. string: filename to copy from
|
||||
2. string: filename to copy to
|
||||
*/
|
||||
CLICON_MSG_KILL, /* Kill (other) session:
|
||||
1. session-id
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ int clicon_rpc_dbitems(clicon_handle h, char *db, char *rx,
|
|||
cvec ***cvv, size_t *cvvlen);
|
||||
int clicon_rpc_save(clicon_handle h, char *dbname, int snapshot, char *filename);
|
||||
int clicon_rpc_load(clicon_handle h, int replace, char *db, char *filename);
|
||||
int clicon_rpc_copy(clicon_handle h, char *db1, char *db2);
|
||||
int clicon_rpc_kill(clicon_handle h, int session_id);
|
||||
int clicon_rpc_debug(clicon_handle h, int level);
|
||||
int clicon_rpc_call(clicon_handle h, uint16_t op, char *plugin, char *func,
|
||||
|
|
|
|||
|
|
@ -101,6 +101,15 @@ clicon_msg_load_decode(struct clicon_msg *msg,
|
|||
int *replace, char **db, char **filename,
|
||||
const char *label);
|
||||
|
||||
struct clicon_msg *
|
||||
clicon_msg_copy_encode(char *db_src, char *db_dst,
|
||||
const char *label);
|
||||
|
||||
int
|
||||
clicon_msg_copy_decode(struct clicon_msg *msg,
|
||||
char **db_src, char **db_dst,
|
||||
const char *label);
|
||||
|
||||
struct clicon_msg *
|
||||
clicon_msg_kill_encode(uint32_t session_id, const char *label);
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ int xmldb_put(clicon_handle h, char *db, cxobj *xt, enum operation_type op);
|
|||
int xmldb_put_xkey(clicon_handle h, char *db,
|
||||
char *xkey, char *val,
|
||||
enum operation_type op);
|
||||
int xmldb_dump(FILE *f, char *dbfilename, char *rxkey);
|
||||
int xmldb_dump_local(FILE *f, char *dbfilename, char *rxkey);
|
||||
int xmldb_copy(clicon_handle h, char *from, char *to);
|
||||
int xmldb_lock(clicon_handle h, char *db, int pid);
|
||||
int xmldb_unlock(clicon_handle h, char *db, int pid);
|
||||
|
|
|
|||
|
|
@ -307,17 +307,17 @@ clicon_file_copy(char *src,
|
|||
return -1;
|
||||
}
|
||||
if((inF = open(src, O_RDONLY)) == -1) {
|
||||
clicon_err(OE_UNIX, errno, "open");
|
||||
clicon_err(OE_UNIX, errno, "open(%s) for read", src);
|
||||
return -1;
|
||||
}
|
||||
if((ouF = open(target, O_WRONLY | O_CREAT | O_TRUNC, st.st_mode)) == -1) {
|
||||
clicon_err(OE_UNIX, errno, "open");
|
||||
clicon_err(OE_UNIX, errno, "open(%s) for write", target);
|
||||
err = errno;
|
||||
goto error;
|
||||
}
|
||||
while((bytes = read(inF, line, sizeof(line))) > 0)
|
||||
if (write(ouF, line, bytes) < 0){
|
||||
clicon_err(OE_UNIX, errno, "write");
|
||||
clicon_err(OE_UNIX, errno, "write(%s)", src);
|
||||
err = errno;
|
||||
goto error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ clicon_rpc_validate(clicon_handle h,
|
|||
* @param[in] value value as string
|
||||
* @retval 0 OK
|
||||
* @retval -1 Error
|
||||
* @note special case: remove all: key:"/" op:OP_REMOVE
|
||||
*/
|
||||
int
|
||||
clicon_rpc_change(clicon_handle h,
|
||||
|
|
@ -284,6 +285,32 @@ clicon_rpc_load(clicon_handle h,
|
|||
return retval;
|
||||
}
|
||||
|
||||
/*! Send a request to backend to copy a file from one location to another
|
||||
* Note this assumes the backend can access these files and (usually) assumes
|
||||
* clients and servers have the access to the same filesystem.
|
||||
* @param[in] h CLICON handle
|
||||
* @param[in] db1 src database, eg "candidate"
|
||||
* @param[in] db2 dst database, eg "running"
|
||||
*/
|
||||
int
|
||||
clicon_rpc_copy(clicon_handle h,
|
||||
char *db1,
|
||||
char *db2)
|
||||
{
|
||||
int retval = -1;
|
||||
struct clicon_msg *msg;
|
||||
|
||||
if ((msg=clicon_msg_copy_encode(db1, db2, __FUNCTION__)) == NULL)
|
||||
goto done;
|
||||
if (clicon_rpc_msg(h, msg, NULL, NULL, NULL, __FUNCTION__) < 0)
|
||||
goto done;
|
||||
retval = 0;
|
||||
done:
|
||||
unchunk_group(__FUNCTION__);
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
||||
/*! Send a kill session request to backend server
|
||||
* @param[in] h CLICON handle
|
||||
* @param[in] session_id Id of session to kill
|
||||
|
|
|
|||
|
|
@ -498,6 +498,64 @@ clicon_msg_load_decode(struct clicon_msg *msg,
|
|||
return 0;
|
||||
}
|
||||
|
||||
struct clicon_msg *
|
||||
clicon_msg_copy_encode(char *db_src, char *db_dst,
|
||||
const char *label)
|
||||
{
|
||||
struct clicon_msg *msg;
|
||||
int hdrlen = sizeof(*msg);
|
||||
uint16_t len;
|
||||
int p;
|
||||
|
||||
clicon_debug(2, "%s: db_src: %s db_dst: %s",
|
||||
__FUNCTION__,
|
||||
db_src, db_dst);
|
||||
p = 0;
|
||||
len = hdrlen + strlen(db_src) + 1 + strlen(db_dst) + 1;
|
||||
if ((msg = (struct clicon_msg *)chunk(len, label)) == NULL){
|
||||
clicon_err(OE_PROTO, errno, "%s: chunk", __FUNCTION__);
|
||||
return NULL;
|
||||
}
|
||||
memset(msg, 0, len);
|
||||
/* hdr */
|
||||
msg->op_type = htons(CLICON_MSG_COPY);
|
||||
msg->op_len = htons(len);
|
||||
/* body */
|
||||
strncpy(msg->op_body+p, db_src, len-p-hdrlen);
|
||||
p += strlen(db_src)+1;
|
||||
strncpy(msg->op_body+p, db_dst, len-p-hdrlen);
|
||||
p += strlen(db_dst)+1;
|
||||
return msg;
|
||||
}
|
||||
|
||||
int
|
||||
clicon_msg_copy_decode(struct clicon_msg *msg,
|
||||
char **db_src, char **db_dst,
|
||||
const char *label)
|
||||
{
|
||||
int p;
|
||||
|
||||
p = 0;
|
||||
/* body */
|
||||
if ((*db_src = chunk_sprintf(label, "%s", msg->op_body+p)) == NULL){
|
||||
clicon_err(OE_PROTO, errno, "%s: chunk_sprintf",
|
||||
__FUNCTION__);
|
||||
return -1;
|
||||
}
|
||||
p += strlen(*db_src)+1;
|
||||
|
||||
if ((*db_dst = chunk_sprintf(label, "%s", msg->op_body+p)) == NULL){
|
||||
clicon_err(OE_PROTO, errno, "%s: chunk_sprintf",
|
||||
__FUNCTION__);
|
||||
return -1;
|
||||
}
|
||||
p += strlen(*db_dst)+1;
|
||||
clicon_debug(2, "%s: db_src: %s db_dst: %s",
|
||||
__FUNCTION__,
|
||||
*db_src, *db_dst);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct clicon_msg *
|
||||
clicon_msg_kill_encode(uint32_t session_id, const char *label)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -105,9 +105,17 @@ db_init(char *file)
|
|||
return db_init_mode(file, DP_OWRITER | DP_OCREAT ); /* DP_OTRUNC? */
|
||||
}
|
||||
|
||||
/*! Remove database by removing file, if it exists *
|
||||
* @param[in] file database file
|
||||
*/
|
||||
int
|
||||
db_delete(char *file)
|
||||
{
|
||||
struct stat sb;
|
||||
|
||||
if (stat(file, &sb) < 0){
|
||||
return 0;
|
||||
}
|
||||
if (unlink(file) < 0){
|
||||
clicon_err(OE_DB, errno, "unlink %s", file);
|
||||
return -1;
|
||||
|
|
|
|||
|
|
@ -594,22 +594,20 @@ get(char *dbname,
|
|||
clicon_err(OE_XML, 0, "Malformed key: %s", xk);
|
||||
goto done;
|
||||
}
|
||||
name = vec[1];
|
||||
if ((y = yang_find_topnode(ys, name)) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "No yang node found: %s", name);
|
||||
goto done;
|
||||
}
|
||||
if ((xc = xml_find(x, name))==NULL)
|
||||
if ((xc = xml_new_spec(name, x, y)) == NULL)
|
||||
goto done;
|
||||
x = xc;
|
||||
i = 2;
|
||||
i = 1;
|
||||
while (i<nvec){
|
||||
name = vec[i];
|
||||
if ((y = yang_find_syntax((yang_node*)y, name)) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "No yang node found: %s", name);
|
||||
goto done;
|
||||
if (i == 1){ /* spec->module->node */
|
||||
if ((y = yang_find_topnode(ys, name)) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "No yang node found: %s", name);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
else
|
||||
if ((y = yang_find_syntax((yang_node*)y, name)) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "No yang node found: %s", name);
|
||||
goto done;
|
||||
}
|
||||
switch (y->ys_keyword){
|
||||
case Y_LEAF_LIST:
|
||||
/*
|
||||
|
|
@ -664,6 +662,8 @@ get(char *dbname,
|
|||
x = xc;
|
||||
} /* while */
|
||||
break;
|
||||
case Y_LEAF:
|
||||
case Y_CONTAINER:
|
||||
default:
|
||||
if ((xc = xml_find(x, name))==NULL)
|
||||
if ((xc = xml_new_spec(name, x, y)) == NULL)
|
||||
|
|
@ -1158,7 +1158,6 @@ xmldb_put_local(clicon_handle h,
|
|||
return retval;
|
||||
}
|
||||
|
||||
|
||||
/*! Modify database provided an xml tree and an operation
|
||||
* @param[in] dbname Name of database to search in (filename including dir path)
|
||||
* @param[in] h CLICON handle
|
||||
|
|
@ -1239,8 +1238,14 @@ xmldb_put_xkey_local(clicon_handle h,
|
|||
while (i<nvec){
|
||||
name = vec[i];
|
||||
if (i==1){
|
||||
if (!strlen(name) && (op==OP_DELETE || op == OP_REMOVE)){
|
||||
/* Special handling of "/" */
|
||||
cprintf(ckey, "/%s", name);
|
||||
break;
|
||||
}
|
||||
else
|
||||
if ((y = yang_find_topnode(yspec, name)) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "No yang node found: %s", xml_name(x));
|
||||
clicon_err(OE_UNIX, errno, "No yang node found: %s", x?xml_name(x):"");
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
|
@ -1281,7 +1286,7 @@ xmldb_put_xkey_local(clicon_handle h,
|
|||
while ((cvi = cvec_each(cvk, cvi)) != NULL) {
|
||||
keyname = cv_string_get(cvi);
|
||||
val2 = vec[i++];
|
||||
if (i>=nvec){
|
||||
if (i>nvec){ /* XXX >= ? */
|
||||
clicon_err(OE_XML, errno, "List %s without argument", name);
|
||||
goto done;
|
||||
}
|
||||
|
|
@ -1291,8 +1296,8 @@ xmldb_put_xkey_local(clicon_handle h,
|
|||
if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE)
|
||||
if (db_set(filename, cbuf_get(csubkey), val2, strlen(val2)+1) < 0)
|
||||
goto done;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE)
|
||||
if (db_set(filename, cbuf_get(ckey), NULL, 0) < 0)
|
||||
|
|
@ -1390,9 +1395,9 @@ xmldb_put_xkey(clicon_handle h,
|
|||
* @note This function can only be called locally.
|
||||
*/
|
||||
int
|
||||
xmldb_dump(FILE *f,
|
||||
char *dbfilename,
|
||||
char *rxkey)
|
||||
xmldb_dump_local(FILE *f,
|
||||
char *dbfilename,
|
||||
char *rxkey)
|
||||
{
|
||||
int retval = -1;
|
||||
int npairs;
|
||||
|
|
@ -1415,6 +1420,7 @@ xmldb_dump(FILE *f,
|
|||
return retval;
|
||||
}
|
||||
|
||||
|
||||
/*! Local variant of xmldb_copy */
|
||||
static int
|
||||
xmldb_copy_local(clicon_handle h,
|
||||
|
|
@ -1619,7 +1625,9 @@ xmldb_delete_local(clicon_handle h,
|
|||
return retval;
|
||||
}
|
||||
|
||||
/*! Delete database. Remove file */
|
||||
/*! Delete database. Remove file
|
||||
* Should not be called from client. Use change("/", OP_REMOVE) instead.
|
||||
*/
|
||||
int
|
||||
xmldb_delete(clicon_handle h,
|
||||
char *db)
|
||||
|
|
|
|||
|
|
@ -448,9 +448,9 @@ yang_find_syntax(yang_node *yn, char *argument)
|
|||
return ysmatch;
|
||||
}
|
||||
|
||||
/*! Help function to check find 'top-node', eg first 'syntax node in a spec
|
||||
/*! Help function to check find 'top-node', eg first 'syntax' node in a spec
|
||||
* A yang specification has modules as children which in turn can have
|
||||
* syntax-nodes as children. This function goes through all the modulers to
|
||||
* syntax-nodes as children. This function goes through all the modules to
|
||||
* look for syntax-nodes. Note that if a child to a module is a choice,
|
||||
* the search is made recursively made to the choice's children.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue