Skip to content
Snippets Groups Projects
extconf.c 164 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*! \brief extract offset:length from variable name.
     * Returns 1 if there is a offset:length part, which is
     * trimmed off (values go into variables)
     */
    static int parse_variable_name(char *var, int *offset, int *length, int *isfunc)
    {
    	int parens=0;
    
    	*offset = 0;
    	*length = INT_MAX;
    	*isfunc = 0;
    	for (; *var; var++) {
    		if (*var == '(') {
    			(*isfunc)++;
    			parens++;
    		} else if (*var == ')') {
    			parens--;
    		} else if (*var == ':' && parens == 0) {
    			*var++ = '\0';
    
    Tilghman Lesher's avatar
    Tilghman Lesher committed
    			sscanf(var, "%30d:%30d", offset, length);
    
    			return 1; /* offset:length valid */
    		}
    	}
    	return 0;
    }
    
    static const char *ast_var_value(const struct ast_var_t *var)
    {
    	return (var ? var->value : NULL);
    }
    
    /*! \brief takes a substring. It is ok to call with value == workspace.
     *
     * offset < 0 means start from the end of the string and set the beginning
     *   to be that many characters back.
     * length is the length of the substring.  A value less than 0 means to leave
     * that many off the end.
     * Always return a copy in workspace.
     */
    static char *substring(const char *value, int offset, int length, char *workspace, size_t workspace_len)
    {
    	char *ret = workspace;
    	int lr;	/* length of the input string after the copy */
    
    	ast_copy_string(workspace, value, workspace_len); /* always make a copy */
    
    	lr = strlen(ret); /* compute length after copy, so we never go out of the workspace */
    
    	/* Quick check if no need to do anything */
    	if (offset == 0 && length >= lr)	/* take the whole string */
    		return ret;
    
    	if (offset < 0)	{	/* translate negative offset into positive ones */
    		offset = lr + offset;
    		if (offset < 0) /* If the negative offset was greater than the length of the string, just start at the beginning */
    			offset = 0;
    	}
    
    	/* too large offset result in empty string so we know what to return */
    	if (offset >= lr)
    		return ret + lr;	/* the final '\0' */
    
    	ret += offset;		/* move to the start position */
    	if (length >= 0 && length < lr - offset)	/* truncate if necessary */
    		ret[length] = '\0';
    	else if (length < 0) {
    		if (lr > offset - length) /* After we remove from the front and from the rear, is there anything left? */
    			ret[lr + length - offset] = '\0';
    		else
    			ret[0] = '\0';
    	}
    
    	return ret;
    }
    
    /*! \brief  Support for Asterisk built-in variables in the dialplan
    \note	See also
    	- \ref AstVar	Channel variables
    	- \ref AstCauses The HANGUPCAUSE variable
     */
    static void pbx_retrieve_variable(struct ast_channel *c, const char *var, char **ret, char *workspace, int workspacelen, struct varshead *headp)
    {
    	const char not_found = '\0';
    	char *tmpvar;
    	const char *s;	/* the result */
    	int offset, length;
    	int i, need_substring;
    	struct varshead *places[2] = { headp, &globals };	/* list of places where we may look */
    
    	/*
    	 * Make a copy of var because parse_variable_name() modifies the string.
    	 * Then if called directly, we might need to run substring() on the result;
    	 * remember this for later in 'need_substring', 'offset' and 'length'
    	 */
    	tmpvar = ast_strdupa(var);	/* parse_variable_name modifies the string */
    	need_substring = parse_variable_name(tmpvar, &offset, &length, &i /* ignored */);
    
    	/*
    	 * Look first into predefined variables, then into variable lists.
    	 * Variable 's' points to the result, according to the following rules:
    	 * s == &not_found (set at the beginning) means that we did not find a
    	 *	matching variable and need to look into more places.
    	 * If s != &not_found, s is a valid result string as follows:
    	 * s = NULL if the variable does not have a value;
    	 *	you typically do this when looking for an unset predefined variable.
    	 * s = workspace if the result has been assembled there;
    	 *	typically done when the result is built e.g. with an snprintf(),
    	 *	so we don't need to do an additional copy.
    	 * s != workspace in case we have a string, that needs to be copied
    	 *	(the ast_copy_string is done once for all at the end).
    	 *	Typically done when the result is already available in some string.
    	 */
    	s = &not_found;	/* default value */
    	if (s == &not_found) { /* look for more */
    		if (!strcmp(var, "EPOCH")) {
    			snprintf(workspace, workspacelen, "%u",(int)time(NULL));
    		}
    
    		s = workspace;
    	}
    	/* if not found, look into chanvars or global vars */
    	for (i = 0; s == &not_found && i < (sizeof(places) / sizeof(places[0])); i++) {
    		struct ast_var_t *variables;
    		if (!places[i])
    			continue;
    		if (places[i] == &globals)
    			ast_rwlock_rdlock(&globalslock);
    		AST_LIST_TRAVERSE(places[i], variables, entries) {
    			if (strcasecmp(ast_var_name(variables), var)==0) {
    				s = ast_var_value(variables);
    				break;
    			}
    		}
    		if (places[i] == &globals)
    			ast_rwlock_unlock(&globalslock);
    	}
    	if (s == &not_found || s == NULL)
    		*ret = NULL;
    	else {
    		if (s != workspace)
    			ast_copy_string(workspace, s, workspacelen);
    		*ret = workspace;
    		if (need_substring)
    			*ret = substring(*ret, offset, length, workspace, workspacelen);
    	}
    }
    
    static void pbx_substitute_variables_helper_full(struct ast_channel *c, struct varshead *headp, const char *cp1, char *cp2, int count)
    {
    	/* Substitutes variables into cp2, based on string cp1, and assuming cp2 to be
    	   zero-filled */
    	char *cp4;
    	const char *tmp, *whereweare;
    	int length, offset, offset2, isfunction;
    	char *workspace = NULL;
    	char *ltmp = NULL, *var = NULL;
    	char *nextvar, *nextexp, *nextthing;
    	char *vars, *vare;
    	int pos, brackets, needsub, len;
    
    
    	*cp2 = 0; /* just in case there's nothing to do */
    
    	whereweare=tmp=cp1;
    	while (!ast_strlen_zero(whereweare) && count) {
    		/* Assume we're copying the whole remaining string */
    		pos = strlen(whereweare);
    		nextvar = NULL;
    		nextexp = NULL;
    		nextthing = strchr(whereweare, '$');
    		if (nextthing) {
    			switch (nextthing[1]) {
    			case '{':
    				nextvar = nextthing;
    				pos = nextvar - whereweare;
    				break;
    			case '[':
    				nextexp = nextthing;
    				pos = nextexp - whereweare;
    				break;
    			}
    		}
    
    		if (pos) {
    			/* Can't copy more than 'count' bytes */
    			if (pos > count)
    				pos = count;
    
    			/* Copy that many bytes */
    			memcpy(cp2, whereweare, pos);
    
    			count -= pos;
    			cp2 += pos;
    			whereweare += pos;
    
    		}
    
    		if (nextvar) {
    			/* We have a variable.  Find the start and end, and determine
    			   if we are going to have to recursively call ourselves on the
    			   contents */
    			vars = vare = nextvar + 2;
    			brackets = 1;
    			needsub = 0;
    
    			/* Find the end of it */
    			while (brackets && *vare) {
    				if ((vare[0] == '$') && (vare[1] == '{')) {
    					needsub++;
    				} else if (vare[0] == '{') {
    					brackets++;
    				} else if (vare[0] == '}') {
    					brackets--;
    				} else if ((vare[0] == '$') && (vare[1] == '['))
    					needsub++;
    				vare++;
    			}
    			if (brackets)
    				ast_log(LOG_NOTICE, "Error in extension logic (missing '}' in '%s')\n", cp1);
    			len = vare - vars - 1;
    
    			/* Skip totally over variable string */
    			whereweare += (len + 3);
    
    			if (!var)
    
    				var = alloca(VAR_BUF_SIZE);
    
    
    			/* Store variable name (and truncate) */
    			ast_copy_string(var, vars, len + 1);
    
    			/* Substitute if necessary */
    			if (needsub) {
    				if (!ltmp)
    
    					ltmp = alloca(VAR_BUF_SIZE);
    
    
    				memset(ltmp, 0, VAR_BUF_SIZE);
    				pbx_substitute_variables_helper_full(c, headp, var, ltmp, VAR_BUF_SIZE - 1);
    				vars = ltmp;
    			} else {
    				vars = var;
    			}
    
    			if (!workspace)
    
    				workspace = alloca(VAR_BUF_SIZE);
    
    
    			workspace[0] = '\0';
    
    			parse_variable_name(vars, &offset, &offset2, &isfunction);
    			if (isfunction) {
    				/* Evaluate function */
    				cp4 = ast_func_read(c, vars, workspace, VAR_BUF_SIZE) ? NULL : workspace;
    
    				if (option_debug)
    					ast_log(LOG_DEBUG, "Function result is '%s'\n", cp4 ? cp4 : "(null)");
    
    			} else {
    				/* Retrieve variable value */
    				pbx_retrieve_variable(c, vars, &cp4, workspace, VAR_BUF_SIZE, headp);
    			}
    			if (cp4) {
    				cp4 = substring(cp4, offset, offset2, workspace, VAR_BUF_SIZE);
    
    				length = strlen(cp4);
    				if (length > count)
    					length = count;
    				memcpy(cp2, cp4, length);
    				count -= length;
    				cp2 += length;
    
    			}
    		} else if (nextexp) {
    			/* We have an expression.  Find the start and end, and determine
    			   if we are going to have to recursively call ourselves on the
    			   contents */
    			vars = vare = nextexp + 2;
    			brackets = 1;
    			needsub = 0;
    
    			/* Find the end of it */
    			while (brackets && *vare) {
    				if ((vare[0] == '$') && (vare[1] == '[')) {
    					needsub++;
    					brackets++;
    					vare++;
    				} else if (vare[0] == '[') {
    					brackets++;
    				} else if (vare[0] == ']') {
    					brackets--;
    				} else if ((vare[0] == '$') && (vare[1] == '{')) {
    					needsub++;
    					vare++;
    				}
    				vare++;
    			}
    			if (brackets)
    				ast_log(LOG_NOTICE, "Error in extension logic (missing ']')\n");
    			len = vare - vars - 1;
    
    			/* Skip totally over expression */
    			whereweare += (len + 3);
    
    			if (!var)
    
    				var = alloca(VAR_BUF_SIZE);
    
    
    			/* Store variable name (and truncate) */
    			ast_copy_string(var, vars, len + 1);
    
    			/* Substitute if necessary */
    			if (needsub) {
    				if (!ltmp)
    
    					ltmp = alloca(VAR_BUF_SIZE);
    
    
    				memset(ltmp, 0, VAR_BUF_SIZE);
    				pbx_substitute_variables_helper_full(c, headp, var, ltmp, VAR_BUF_SIZE - 1);
    				vars = ltmp;
    			} else {
    				vars = var;
    			}
    
    			length = ast_expr(vars, cp2, count, NULL);
    
    			if (length) {
    
    				if (option_debug)
    					ast_log(LOG_DEBUG, "Expression result is '%s'\n", cp2);
    
    			}
    		} else
    			break;
    	}
    }
    
    static void pbx_substitute_variables_helper(struct ast_channel *c, const char *cp1, char *cp2, int count)
    {
    	pbx_substitute_variables_helper_full(c, NULL, cp1, cp2, count);
    }
    
    
    static int pbx_load_config(const char *config_file);
    
    static int pbx_load_config(const char *config_file)
    {
    	struct ast_config *cfg;
    	char *end;
    	char *label;
    	char realvalue[256];
    	int lastpri = -2;
    	struct ast_context *con;
    	struct ast_variable *v;
    	const char *cxt;
    	const char *aft;
    
    	cfg = localized_config_load(config_file);
    	if (!cfg)
    		return 0;
    
    	/* Use existing config to populate the PBX table */
    	static_config = ast_true(ast_variable_retrieve(cfg, "general", "static"));
    	write_protect_config = ast_true(ast_variable_retrieve(cfg, "general", "writeprotect"));
    	if ((aft = ast_variable_retrieve(cfg, "general", "autofallthrough")))
    		autofallthrough_config = ast_true(aft);
    	clearglobalvars_config = ast_true(ast_variable_retrieve(cfg, "general", "clearglobalvars"));
    
    
    	if ((cxt = ast_variable_retrieve(cfg, "general", "userscontext")))
    
    		ast_copy_string(userscontext, cxt, sizeof(userscontext));
    	else
    		ast_copy_string(userscontext, "default", sizeof(userscontext));
    
    	for (v = ast_variable_browse(cfg, "globals"); v; v = v->next) {
    		memset(realvalue, 0, sizeof(realvalue));
    		pbx_substitute_variables_helper(NULL, v->value, realvalue, sizeof(realvalue) - 1);
    		pbx_builtin_setvar_helper(NULL, v->name, realvalue);
    	}
    	for (cxt = NULL; (cxt = ast_category_browse(cfg, cxt)); ) {
    		/* All categories but "general" or "globals" are considered contexts */
    		if (!strcasecmp(cxt, "general") || !strcasecmp(cxt, "globals"))
    			continue;
    
    		con=ast_context_find_or_create(&local_contexts,NULL,cxt, global_registrar);
    
    		if (con == NULL)
    			continue;
    
    		for (v = ast_variable_browse(cfg, cxt); v; v = v->next) {
    			if (!strcasecmp(v->name, "exten")) {
    				char *tc = ast_strdup(v->value);
    				if (tc) {
    					int ipri = -2;
    					char realext[256]="";
    					char *plus, *firstp, *firstc;
    					char *pri, *appl, *data, *cidmatch;
    					char *stringp = tc;
    					char *ext = strsep(&stringp, ",");
    					if (!ext)
    						ext="";
    					pbx_substitute_variables_helper(NULL, ext, realext, sizeof(realext) - 1);
    					cidmatch = strchr(realext, '/');
    					if (cidmatch) {
    						*cidmatch++ = '\0';
    						ast_shrink_phone_number(cidmatch);
    					}
    					pri = strsep(&stringp, ",");
    					if (!pri)
    						pri="";
    					label = strchr(pri, '(');
    					if (label) {
    						*label++ = '\0';
    						end = strchr(label, ')');
    						if (end)
    							*end = '\0';
    						else
    							ast_log(LOG_WARNING, "Label missing trailing ')' at line %d\n", v->lineno);
    					}
    					plus = strchr(pri, '+');
    					if (plus)
    						*plus++ = '\0';
    					if (!strcmp(pri,"hint"))
    						ipri=PRIORITY_HINT;
    					else if (!strcmp(pri, "next") || !strcmp(pri, "n")) {
    						if (lastpri > -2)
    							ipri = lastpri + 1;
    						else
    							ast_log(LOG_WARNING, "Can't use 'next' priority on the first entry!\n");
    					} else if (!strcmp(pri, "same") || !strcmp(pri, "s")) {
    						if (lastpri > -2)
    							ipri = lastpri;
    						else
    							ast_log(LOG_WARNING, "Can't use 'same' priority on the first entry!\n");
    
    Tilghman Lesher's avatar
    Tilghman Lesher committed
    					} else if (sscanf(pri, "%30d", &ipri) != 1 &&
    
    					    (ipri = ast_findlabel_extension2(NULL, con, realext, pri, cidmatch)) < 1) {
    						ast_log(LOG_WARNING, "Invalid priority/label '%s' at line %d\n", pri, v->lineno);
    						ipri = 0;
    					}
    					appl = S_OR(stringp, "");
    					/* Find the first occurrence of either '(' or ',' */
    					firstc = strchr(appl, ',');
    					firstp = strchr(appl, '(');
    					if (firstc && (!firstp || firstc < firstp)) {
    						/* comma found, no parenthesis */
    						/* or both found, but comma found first */
    						appl = strsep(&stringp, ",");
    						data = stringp;
    					} else if (!firstc && !firstp) {
    						/* Neither found */
    						data = "";
    					} else {
    						/* Final remaining case is parenthesis found first */
    						appl = strsep(&stringp, "(");
    						data = stringp;
    						end = strrchr(data, ')');
    						if ((end = strrchr(data, ')'))) {
    							*end = '\0';
    						} else {
    							ast_log(LOG_WARNING, "No closing parenthesis found? '%s(%s'\n", appl, data);
    						}
    						ast_process_quotes_and_slashes(data, ',', '|');
    					}
    
    					if (!data)
    						data="";
    					appl = ast_skip_blanks(appl);
    					if (ipri) {
    						if (plus)
    							ipri += atoi(plus);
    						lastpri = ipri;
    						if (!ast_opt_dont_warn && !strcmp(realext, "_."))
    							ast_log(LOG_WARNING, "The use of '_.' for an extension is strongly discouraged and can have unexpected behavior.  Please use '_X.' instead at line %d\n", v->lineno);
    
    						if (ast_add_extension2(con, 0, realext, ipri, label, cidmatch, appl, strdup(data), ast_free_ptr, global_registrar)) {
    
    							ast_log(LOG_WARNING, "Unable to register extension at line %d\n", v->lineno);
    						}
    					}
    					free(tc);
    				}
    			} else if (!strcasecmp(v->name, "include")) {
    				memset(realvalue, 0, sizeof(realvalue));
    				pbx_substitute_variables_helper(NULL, v->value, realvalue, sizeof(realvalue) - 1);
    
    				if (ast_context_add_include2(con, realvalue, global_registrar))
    
    					ast_log(LOG_WARNING, "Unable to include context '%s' in context '%s'\n", v->value, cxt);
    			} else if (!strcasecmp(v->name, "ignorepat")) {
    				memset(realvalue, 0, sizeof(realvalue));
    				pbx_substitute_variables_helper(NULL, v->value, realvalue, sizeof(realvalue) - 1);
    
    				if (ast_context_add_ignorepat2(con, realvalue, global_registrar))
    
    					ast_log(LOG_WARNING, "Unable to include ignorepat '%s' in context '%s'\n", v->value, cxt);
    			} else if (!strcasecmp(v->name, "switch") || !strcasecmp(v->name, "lswitch") || !strcasecmp(v->name, "eswitch")) {
    				char *stringp= realvalue;
    				char *appl, *data;
    
    				memset(realvalue, 0, sizeof(realvalue));
    				if (!strcasecmp(v->name, "switch"))
    					pbx_substitute_variables_helper(NULL, v->value, realvalue, sizeof(realvalue) - 1);
    				else
    					ast_copy_string(realvalue, v->value, sizeof(realvalue));
    				appl = strsep(&stringp, "/");
    				data = strsep(&stringp, ""); /* XXX what for ? */
    				if (!data)
    					data = "";
    
    				if (ast_context_add_switch2(con, appl, data, !strcasecmp(v->name, "eswitch"), global_registrar))
    
    					ast_log(LOG_WARNING, "Unable to include switch '%s' in context '%s'\n", v->value, cxt);
    			} else {
    				ast_log(LOG_WARNING, "==!!== Unknown directive: %s at line %d -- IGNORING!!!\n", v->name, v->lineno);
    			}
    		}
    	}
    	ast_config_destroy(cfg);
    	return 1;
    }
    
    static void __ast_context_destroy(struct ast_context *con, const char *registrar)
    {
    	struct ast_context *tmp, *tmpl=NULL;
    	struct ast_include *tmpi;
    	struct ast_sw *sw;
    	struct ast_exten *e, *el, *en;
    	struct ast_ignorepat *ipi;
    
    	for (tmp = contexts; tmp; ) {
    		struct ast_context *next;	/* next starting point */
    		for (; tmp; tmpl = tmp, tmp = tmp->next) {
    
    			if (option_debug)
    				ast_log(LOG_DEBUG, "check ctx %s %s\n", tmp->name, tmp->registrar);
    
    			if ( (!registrar || !strcasecmp(registrar, tmp->registrar)) &&
    			     (!con || !strcasecmp(tmp->name, con->name)) )
    				break;	/* found it */
    		}
    		if (!tmp)	/* not found, we are done */
    			break;
    		ast_wrlock_context(tmp);
    
    		if (option_debug)
    			ast_log(LOG_DEBUG, "delete ctx %s %s\n", tmp->name, tmp->registrar);
    
    		next = tmp->next;
    		if (tmpl)
    			tmpl->next = next;
    		else
    			contexts = next;
    		/* Okay, now we're safe to let it go -- in a sense, we were
    		   ready to let it go as soon as we locked it. */
    		ast_unlock_context(tmp);
    		for (tmpi = tmp->includes; tmpi; ) { /* Free includes */
    			struct ast_include *tmpil = tmpi;
    			tmpi = tmpi->next;
    			free(tmpil);
    		}
    		for (ipi = tmp->ignorepats; ipi; ) { /* Free ignorepats */
    			struct ast_ignorepat *ipl = ipi;
    			ipi = ipi->next;
    			free(ipl);
    		}
    		while ((sw = AST_LIST_REMOVE_HEAD(&tmp->alts, list)))
    			free(sw);
    		for (e = tmp->root; e;) {
    			for (en = e->peer; en;) {
    				el = en;
    				en = en->peer;
    				destroy_exten(el);
    			}
    			el = e;
    			e = e->next;
    			destroy_exten(el);
    		}
    		ast_rwlock_destroy(&tmp->lock);
    		free(tmp);
    		/* if we have a specific match, we are done, otherwise continue */
    		tmp = con ? NULL : next;
    	}
    }
    
    void localized_context_destroy(struct ast_context *con, const char *registrar);
    
    void localized_context_destroy(struct ast_context *con, const char *registrar)
    {
    	ast_wrlock_contexts();
    	__ast_context_destroy(con,registrar);
    	ast_unlock_contexts();
    }
    
    
    static void ast_merge_contexts_and_delete(struct ast_context **extcontexts, const char *registrar)
    {
    	struct ast_context *tmp, *lasttmp = NULL;
    
    	/* it is very important that this function hold the hint list lock _and_ the conlock
    	   during its operation; not only do we need to ensure that the list of contexts
    	   and extensions does not change, but also that no hint callbacks (watchers) are
    	   added or removed during the merge/delete process
    
    	   in addition, the locks _must_ be taken in this order, because there are already
    	   other code paths that use this order
    	*/
    	ast_wrlock_contexts();
    
    	tmp = *extcontexts;
    	if (registrar) {
    		/* XXX remove previous contexts from same registrar */
    
    		if (option_debug)
    			ast_log(LOG_DEBUG, "must remove any reg %s\n", registrar);
    
    		__ast_context_destroy(NULL,registrar);
    		while (tmp) {
    			lasttmp = tmp;
    			tmp = tmp->next;
    		}
    	} else {
    		/* XXX remove contexts with the same name */
    		while (tmp) {
    			ast_log(LOG_WARNING, "must remove %s  reg %s\n", tmp->name, tmp->registrar);
    			__ast_context_destroy(tmp,tmp->registrar);
    			lasttmp = tmp;
    			tmp = tmp->next;
    		}
    	}
    	if (lasttmp) {
    		lasttmp->next = contexts;
    		contexts = *extcontexts;
    		*extcontexts = NULL;
    	} else
    		ast_log(LOG_WARNING, "Requested contexts didn't get merged\n");
    
    	ast_unlock_contexts();
    
    	return;
    }
    
    
    void localized_merge_contexts_and_delete(struct ast_context **extcontexts, void *tab, const char *registrar)
    
    {
    	ast_merge_contexts_and_delete(extcontexts, registrar);
    }
    
    static int ast_context_verify_includes(struct ast_context *con)
    {
    	struct ast_include *inc = NULL;
    	int res = 0;
    
    	while ( (inc = ast_walk_context_includes(con, inc)) )
    		if (!ast_context_find(inc->rname)) {
    			res = -1;
    			if (strcasecmp(inc->rname,"parkedcalls")!=0)
    				ast_log(LOG_WARNING, "Context '%s' tries to include the nonexistent context '%s'\n",
    						ast_get_context_name(con), inc->rname);
    		}
    	return res;
    }
    
    int localized_context_verify_includes(struct ast_context *con);
    
    int localized_context_verify_includes(struct ast_context *con)
    {
    	return ast_context_verify_includes(con);
    }
    
    int localized_pbx_load_module(void);
    
    int localized_pbx_load_module(void)
    {
    	struct ast_context *con;
    
    
    	if(!pbx_load_config(config_filename))
    
    		return -1 /* AST_MODULE_LOAD_DECLINE*/;
    
    	/* pbx_load_users(); */ /* does this affect the dialplan? */
    
    
    	ast_merge_contexts_and_delete(&local_contexts, global_registrar);
    
    
    	for (con = NULL; (con = ast_walk_contexts(con));)
    		ast_context_verify_includes(con);
    
    	printf("=== Loading extensions.conf ===\n");
    	con = 0;
    	while ((con = ast_walk_contexts(con)) ) {
    		printf("Context: %s\n", con->name);
    	}
    	printf("=========\n");
    
    /* For platforms which don't have pthread_rwlock_timedrdlock() */
    struct timeval ast_tvnow(void);
    
    struct timeval ast_tvnow(void)
    {
    	struct timeval t;
    	gettimeofday(&t, NULL);
    	return t;
    }