2 * Asterisk -- An open source telephony toolkit.
4 * Copyright (C) 1999 - 2005, Digium, Inc.
6 * Mark Spencer <markster@digium.com>
8 * res_odbc.c <ODBC resource manager>
9 * Copyright (C) 2004 - 2005 Anthony Minessale II <anthmct@yahoo.com>
11 * See http://www.asterisk.org for more information about
12 * the Asterisk project. Please do not directly contact
13 * any of the maintainers of this project for assistance;
14 * the project provides a web site, mailing lists and IRC
15 * channels for your use.
17 * This program is free software, distributed under the terms of
18 * the GNU General Public License Version 2. See the LICENSE file
19 * at the top of the source tree.
24 * \brief ODBC resource manager
26 * \author Mark Spencer <markster@digium.com>
27 * \author Anthony Minessale II <anthmct@yahoo.com>
29 * \arg See also: \ref cdr_odbc
33 <depend>unixodbc</depend>
39 ASTERISK_FILE_VERSION(__FILE__
, "$Revision$")
46 #include "asterisk/file.h"
47 #include "asterisk/logger.h"
48 #include "asterisk/channel.h"
49 #include "asterisk/config.h"
50 #include "asterisk/options.h"
51 #include "asterisk/pbx.h"
52 #include "asterisk/module.h"
53 #include "asterisk/cli.h"
54 #include "asterisk/lock.h"
55 #include "asterisk/res_odbc.h"
56 #include "asterisk/time.h"
60 AST_LIST_ENTRY(odbc_class
) list
;
66 unsigned int haspool
:1; /* Boolean - TDS databases need this */
67 unsigned int limit
:10; /* Gives a limit of 1023 maximum */
68 unsigned int count
:10; /* Running count of pooled connections */
69 unsigned int delme
:1; /* Purge the class */
70 unsigned int backslash_is_escape
:1; /* On this database, the backslash is a native escape sequence */
71 unsigned int idlecheck
; /* Recheck the connection if it is idle for this long */
72 AST_LIST_HEAD(, odbc_obj
) odbc_obj
;
75 AST_LIST_HEAD_STATIC(odbc_list
, odbc_class
);
77 static odbc_status
odbc_obj_connect(struct odbc_obj
*obj
);
78 static odbc_status
odbc_obj_disconnect(struct odbc_obj
*obj
);
79 static int odbc_register_class(struct odbc_class
*class, int connect
);
82 SQLHSTMT
ast_odbc_prepare_and_execute(struct odbc_obj
*obj
, SQLHSTMT (*prepare_cb
)(struct odbc_obj
*obj
, void *data
), void *data
)
84 int res
= 0, i
, attempt
;
85 SQLINTEGER nativeerror
=0, numfields
=0;
86 SQLSMALLINT diagbytes
=0;
87 unsigned char state
[10], diagnostic
[256];
90 for (attempt
= 0; attempt
< 2; attempt
++) {
91 /* This prepare callback may do more than just prepare -- it may also
92 * bind parameters, bind results, etc. The real key, here, is that
93 * when we disconnect, all handles become invalid for most databases.
94 * We must therefore redo everything when we establish a new
96 stmt
= prepare_cb(obj
, data
);
99 res
= SQLExecute(stmt
);
100 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
) && (res
!= SQL_NO_DATA
)) {
101 if (res
== SQL_ERROR
) {
102 SQLGetDiagField(SQL_HANDLE_STMT
, stmt
, 1, SQL_DIAG_NUMBER
, &numfields
, SQL_IS_INTEGER
, &diagbytes
);
103 for (i
= 0; i
< numfields
; i
++) {
104 SQLGetDiagRec(SQL_HANDLE_STMT
, stmt
, i
+ 1, state
, &nativeerror
, diagnostic
, sizeof(diagnostic
), &diagbytes
);
105 ast_log(LOG_WARNING
, "SQL Execute returned an error %d: %s: %s (%d)\n", res
, state
, diagnostic
, diagbytes
);
107 ast_log(LOG_WARNING
, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields
);
113 ast_log(LOG_WARNING
, "SQL Execute error %d! Attempting a reconnect...\n", res
);
114 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
119 * While this isn't the best way to try to correct an error, this won't automatically
120 * fail when the statement handle invalidates.
122 /* XXX Actually, it might, if we're using a non-pooled connection. Possible race here. XXX */
123 odbc_obj_disconnect(obj
);
124 odbc_obj_connect(obj
);
127 obj
->last_used
= ast_tvnow();
130 ast_log(LOG_WARNING
, "SQL Prepare failed. Attempting a reconnect...\n");
131 odbc_obj_disconnect(obj
);
132 odbc_obj_connect(obj
);
139 int ast_odbc_smart_execute(struct odbc_obj
*obj
, SQLHSTMT stmt
)
142 SQLINTEGER nativeerror
=0, numfields
=0;
143 SQLSMALLINT diagbytes
=0;
144 unsigned char state
[10], diagnostic
[256];
146 res
= SQLExecute(stmt
);
147 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
) && (res
!= SQL_NO_DATA
)) {
148 if (res
== SQL_ERROR
) {
149 SQLGetDiagField(SQL_HANDLE_STMT
, stmt
, 1, SQL_DIAG_NUMBER
, &numfields
, SQL_IS_INTEGER
, &diagbytes
);
150 for (i
= 0; i
< numfields
; i
++) {
151 SQLGetDiagRec(SQL_HANDLE_STMT
, stmt
, i
+ 1, state
, &nativeerror
, diagnostic
, sizeof(diagnostic
), &diagbytes
);
152 ast_log(LOG_WARNING
, "SQL Execute returned an error %d: %s: %s (%d)\n", res
, state
, diagnostic
, diagbytes
);
154 ast_log(LOG_WARNING
, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields
);
160 /* This is a really bad method of trying to correct a dead connection. It
161 * only ever really worked with MySQL. It will not work with any other
162 * database, since most databases prepare their statements on the server,
163 * and if you disconnect, you invalidate the statement handle. Hence, if
164 * you disconnect, you're going to fail anyway, whether you try to execute
165 * a second time or not.
167 ast_log(LOG_WARNING
, "SQL Execute error %d! Attempting a reconnect...\n", res
);
168 ast_mutex_lock(&obj
->lock
);
170 ast_mutex_unlock(&obj
->lock
);
171 odbc_obj_disconnect(obj
);
172 odbc_obj_connect(obj
);
173 res
= SQLExecute(stmt
);
176 obj
->last_used
= ast_tvnow();
182 int ast_odbc_sanity_check(struct odbc_obj
*obj
)
184 char *test_sql
= "select 1";
189 res
= SQLAllocHandle(SQL_HANDLE_STMT
, obj
->con
, &stmt
);
190 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
193 res
= SQLPrepare(stmt
, (unsigned char *)test_sql
, SQL_NTS
);
194 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
197 res
= SQLExecute(stmt
);
198 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
203 SQLFreeHandle (SQL_HANDLE_STMT
, stmt
);
206 if (!obj
->up
) { /* Try to reconnect! */
207 ast_log(LOG_WARNING
, "Connection is down attempting to reconnect...\n");
208 odbc_obj_disconnect(obj
);
209 odbc_obj_connect(obj
);
214 static int load_odbc_config(void)
216 static char *cfg
= "res_odbc.conf";
217 struct ast_config
*config
;
218 struct ast_variable
*v
;
219 char *cat
, *dsn
, *username
, *password
;
220 int enabled
, pooling
, limit
, bse
;
221 unsigned int idlecheck
;
222 int connect
= 0, res
= 0;
224 struct odbc_class
*new;
226 config
= ast_config_load(cfg
);
228 ast_log(LOG_WARNING
, "Unable to load config file res_odbc.conf\n");
231 for (cat
= ast_category_browse(config
, NULL
); cat
; cat
=ast_category_browse(config
, cat
)) {
232 if (!strcasecmp(cat
, "ENV")) {
233 for (v
= ast_variable_browse(config
, cat
); v
; v
= v
->next
) {
234 setenv(v
->name
, v
->value
, 1);
235 ast_log(LOG_NOTICE
, "Adding ENV var: %s=%s\n", v
->name
, v
->value
);
238 /* Reset all to defaults for each class of odbc connections */
239 dsn
= username
= password
= NULL
;
241 connect
= idlecheck
= 0;
245 for (v
= ast_variable_browse(config
, cat
); v
; v
= v
->next
) {
246 if (!strcasecmp(v
->name
, "pooling")) {
247 if (ast_true(v
->value
))
249 } else if (!strcasecmp(v
->name
, "limit")) {
250 sscanf(v
->value
, "%d", &limit
);
251 if (ast_true(v
->value
) && !limit
) {
252 ast_log(LOG_WARNING
, "Limit should be a number, not a boolean: '%s'. Setting limit to 1023 for ODBC class '%s'.\n", v
->value
, cat
);
254 } else if (ast_false(v
->value
)) {
255 ast_log(LOG_WARNING
, "Limit should be a number, not a boolean: '%s'. Disabling ODBC class '%s'.\n", v
->value
, cat
);
259 } else if (!strcasecmp(v
->name
, "idlecheck")) {
260 sscanf(v
->value
, "%d", &idlecheck
);
261 } else if (!strcasecmp(v
->name
, "enabled")) {
262 enabled
= ast_true(v
->value
);
263 } else if (!strcasecmp(v
->name
, "pre-connect")) {
264 connect
= ast_true(v
->value
);
265 } else if (!strcasecmp(v
->name
, "dsn")) {
267 } else if (!strcasecmp(v
->name
, "username")) {
269 } else if (!strcasecmp(v
->name
, "password")) {
271 } else if (!strcasecmp(v
->name
, "backslash_is_escape")) {
272 bse
= ast_true(v
->value
);
276 if (enabled
&& !ast_strlen_zero(dsn
)) {
277 new = ast_calloc(1, sizeof(*new));
285 ast_copy_string(new->name
, cat
, sizeof(new->name
));
287 ast_copy_string(new->dsn
, dsn
, sizeof(new->dsn
));
289 ast_copy_string(new->username
, username
, sizeof(new->username
));
291 ast_copy_string(new->password
, password
, sizeof(new->password
));
293 SQLAllocHandle(SQL_HANDLE_ENV
, SQL_NULL_HANDLE
, &new->env
);
294 res
= SQLSetEnvAttr(new->env
, SQL_ATTR_ODBC_VERSION
, (void *) SQL_OV_ODBC3
, 0);
296 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
297 ast_log(LOG_WARNING
, "res_odbc: Error SetEnv\n");
298 SQLFreeHandle(SQL_HANDLE_ENV
, new->env
);
303 new->haspool
= pooling
;
307 ast_log(LOG_WARNING
, "Pooling without also setting a limit is pointless. Changing limit from 0 to 5.\n");
312 new->backslash_is_escape
= bse
? 1 : 0;
313 new->idlecheck
= idlecheck
;
315 odbc_register_class(new, connect
);
316 ast_log(LOG_NOTICE
, "Registered ODBC class '%s' dsn->[%s]\n", cat
, dsn
);
320 ast_config_destroy(config
);
324 static int odbc_show_command(int fd
, int argc
, char **argv
)
326 struct odbc_class
*class;
327 struct odbc_obj
*current
;
329 AST_LIST_LOCK(&odbc_list
);
330 AST_LIST_TRAVERSE(&odbc_list
, class, list
) {
331 if ((argc
== 2) || (argc
== 3 && !strcmp(argv
[2], "all")) || (!strcmp(argv
[2], class->name
))) {
333 ast_cli(fd
, "Name: %s\nDSN: %s\n", class->name
, class->dsn
);
335 if (class->haspool
) {
336 ast_cli(fd
, "Pooled: yes\nLimit: %d\nConnections in use: %d\n", class->limit
, class->count
);
338 AST_LIST_TRAVERSE(&(class->odbc_obj
), current
, list
) {
339 ast_cli(fd
, " Connection %d: %s\n", ++count
, current
->used
? "in use" : current
->up
&& ast_odbc_sanity_check(current
) ? "connected" : "disconnected");
342 /* Should only ever be one of these */
343 AST_LIST_TRAVERSE(&(class->odbc_obj
), current
, list
) {
344 ast_cli(fd
, "Pooled: no\nConnected: %s\n", current
->up
&& ast_odbc_sanity_check(current
) ? "yes" : "no");
351 AST_LIST_UNLOCK(&odbc_list
);
356 static char show_usage
[] =
357 "Usage: odbc show [<class>]\n"
358 " List settings of a particular ODBC class.\n"
359 " or, if not specified, all classes.\n";
361 static struct ast_cli_entry cli_odbc
[] = {
362 { { "odbc", "show", NULL
},
363 odbc_show_command
, "List ODBC DSN(s)",
367 static int odbc_register_class(struct odbc_class
*class, int connect
)
369 struct odbc_obj
*obj
;
371 AST_LIST_LOCK(&odbc_list
);
372 AST_LIST_INSERT_HEAD(&odbc_list
, class, list
);
373 AST_LIST_UNLOCK(&odbc_list
);
376 /* Request and release builds a connection */
377 obj
= ast_odbc_request_obj(class->name
, 0);
379 ast_odbc_release_obj(obj
);
384 ast_log(LOG_WARNING
, "Attempted to register a NULL class?\n");
389 void ast_odbc_release_obj(struct odbc_obj
*obj
)
391 /* For pooled connections, this frees the connection to be
392 * reused. For non-pooled connections, it does nothing. */
396 int ast_odbc_backslash_is_escape(struct odbc_obj
*obj
)
398 return obj
->parent
->backslash_is_escape
;
401 struct odbc_obj
*ast_odbc_request_obj(const char *name
, int check
)
403 struct odbc_obj
*obj
= NULL
;
404 struct odbc_class
*class;
406 AST_LIST_LOCK(&odbc_list
);
407 AST_LIST_TRAVERSE(&odbc_list
, class, list
) {
408 if (!strcmp(class->name
, name
))
411 AST_LIST_UNLOCK(&odbc_list
);
416 AST_LIST_LOCK(&class->odbc_obj
);
417 if (class->haspool
) {
418 /* Recycle connections before building another */
419 AST_LIST_TRAVERSE(&class->odbc_obj
, obj
, list
) {
426 if (!obj
&& (class->count
< class->limit
)) {
428 obj
= ast_calloc(1, sizeof(*obj
));
430 AST_LIST_UNLOCK(&class->odbc_obj
);
433 ast_mutex_init(&obj
->lock
);
435 if (odbc_obj_connect(obj
) == ODBC_FAIL
) {
436 ast_log(LOG_WARNING
, "Failed to connect to %s\n", name
);
437 ast_mutex_destroy(&obj
->lock
);
443 AST_LIST_INSERT_TAIL(&class->odbc_obj
, obj
, list
);
447 /* Non-pooled connection: multiple modules can use the same connection. */
448 AST_LIST_TRAVERSE(&class->odbc_obj
, obj
, list
) {
449 /* Non-pooled connection: if there is an entry, return it */
454 /* No entry: build one */
455 obj
= ast_calloc(1, sizeof(*obj
));
457 AST_LIST_UNLOCK(&class->odbc_obj
);
460 ast_mutex_init(&obj
->lock
);
462 if (odbc_obj_connect(obj
) == ODBC_FAIL
) {
463 ast_log(LOG_WARNING
, "Failed to connect to %s\n", name
);
464 ast_mutex_destroy(&obj
->lock
);
468 AST_LIST_INSERT_HEAD(&class->odbc_obj
, obj
, list
);
472 AST_LIST_UNLOCK(&class->odbc_obj
);
475 ast_odbc_sanity_check(obj
);
476 } else if (obj
&& obj
->parent
->idlecheck
> 0 && ast_tvdiff_ms(ast_tvnow(), obj
->last_used
) / 1000 > obj
->parent
->idlecheck
)
477 odbc_obj_connect(obj
);
482 static odbc_status
odbc_obj_disconnect(struct odbc_obj
*obj
)
485 ast_mutex_lock(&obj
->lock
);
487 res
= SQLDisconnect(obj
->con
);
489 if (res
== ODBC_SUCCESS
) {
490 ast_log(LOG_WARNING
, "res_odbc: disconnected %d from %s [%s]\n", res
, obj
->parent
->name
, obj
->parent
->dsn
);
492 ast_log(LOG_WARNING
, "res_odbc: %s [%s] already disconnected\n",
493 obj
->parent
->name
, obj
->parent
->dsn
);
496 ast_mutex_unlock(&obj
->lock
);
500 static odbc_status
odbc_obj_connect(struct odbc_obj
*obj
)
505 unsigned char msg
[200], stat
[10];
507 SQLINTEGER enable
= 1;
508 char *tracefile
= "/tmp/odbc.trace";
510 ast_mutex_lock(&obj
->lock
);
512 res
= SQLAllocHandle(SQL_HANDLE_DBC
, obj
->parent
->env
, &obj
->con
);
514 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
515 ast_log(LOG_WARNING
, "res_odbc: Error AllocHDB %d\n", res
);
516 ast_mutex_unlock(&obj
->lock
);
519 SQLSetConnectAttr(obj
->con
, SQL_LOGIN_TIMEOUT
, (SQLPOINTER
*) 10, 0);
520 SQLSetConnectAttr(obj
->con
, SQL_ATTR_CONNECTION_TIMEOUT
, (SQLPOINTER
*) 10, 0);
522 SQLSetConnectAttr(obj
->con
, SQL_ATTR_TRACE
, &enable
, SQL_IS_INTEGER
);
523 SQLSetConnectAttr(obj
->con
, SQL_ATTR_TRACEFILE
, tracefile
, strlen(tracefile
));
527 odbc_obj_disconnect(obj
);
528 ast_log(LOG_NOTICE
, "Re-connecting %s\n", obj
->parent
->name
);
530 ast_log(LOG_NOTICE
, "Connecting %s\n", obj
->parent
->name
);
533 res
= SQLConnect(obj
->con
,
534 (SQLCHAR
*) obj
->parent
->dsn
, SQL_NTS
,
535 (SQLCHAR
*) obj
->parent
->username
, SQL_NTS
,
536 (SQLCHAR
*) obj
->parent
->password
, SQL_NTS
);
538 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
539 SQLGetDiagRec(SQL_HANDLE_DBC
, obj
->con
, 1, stat
, &err
, msg
, 100, &mlen
);
540 ast_mutex_unlock(&obj
->lock
);
541 ast_log(LOG_WARNING
, "res_odbc: Error SQLConnect=%d errno=%d %s\n", res
, (int)err
, msg
);
544 ast_log(LOG_NOTICE
, "res_odbc: Connected to %s [%s]\n", obj
->parent
->name
, obj
->parent
->dsn
);
546 obj
->last_used
= ast_tvnow();
549 ast_mutex_unlock(&obj
->lock
);
553 static int reload(void)
555 static char *cfg
= "res_odbc.conf";
556 struct ast_config
*config
;
557 struct ast_variable
*v
;
558 char *cat
, *dsn
, *username
, *password
;
559 int enabled
, pooling
, limit
, bse
;
560 unsigned int idlecheck
;
561 int connect
= 0, res
= 0;
563 struct odbc_class
*new, *class;
564 struct odbc_obj
*current
;
566 /* First, mark all to be purged */
567 AST_LIST_LOCK(&odbc_list
);
568 AST_LIST_TRAVERSE(&odbc_list
, class, list
) {
572 config
= ast_config_load(cfg
);
574 for (cat
= ast_category_browse(config
, NULL
); cat
; cat
=ast_category_browse(config
, cat
)) {
575 if (!strcasecmp(cat
, "ENV")) {
576 for (v
= ast_variable_browse(config
, cat
); v
; v
= v
->next
) {
577 setenv(v
->name
, v
->value
, 1);
578 ast_log(LOG_NOTICE
, "Adding ENV var: %s=%s\n", v
->name
, v
->value
);
581 /* Reset all to defaults for each class of odbc connections */
582 dsn
= username
= password
= NULL
;
584 connect
= idlecheck
= 0;
588 for (v
= ast_variable_browse(config
, cat
); v
; v
= v
->next
) {
589 if (!strcasecmp(v
->name
, "pooling")) {
591 } else if (!strcasecmp(v
->name
, "limit")) {
592 sscanf(v
->value
, "%d", &limit
);
593 if (ast_true(v
->value
) && !limit
) {
594 ast_log(LOG_WARNING
, "Limit should be a number, not a boolean: '%s'. Setting limit to 1023 for ODBC class '%s'.\n", v
->value
, cat
);
596 } else if (ast_false(v
->value
)) {
597 ast_log(LOG_WARNING
, "Limit should be a number, not a boolean: '%s'. Disabling ODBC class '%s'.\n", v
->value
, cat
);
601 } else if (!strcasecmp(v
->name
, "idlecheck")) {
602 sscanf(v
->value
, "%ud", &idlecheck
);
603 } else if (!strcasecmp(v
->name
, "enabled")) {
604 enabled
= ast_true(v
->value
);
605 } else if (!strcasecmp(v
->name
, "pre-connect")) {
606 connect
= ast_true(v
->value
);
607 } else if (!strcasecmp(v
->name
, "dsn")) {
609 } else if (!strcasecmp(v
->name
, "username")) {
611 } else if (!strcasecmp(v
->name
, "password")) {
613 } else if (!strcasecmp(v
->name
, "backslash_is_escape")) {
614 bse
= ast_true(v
->value
);
618 if (enabled
&& !ast_strlen_zero(dsn
)) {
619 /* First, check the list to see if it already exists */
620 AST_LIST_TRAVERSE(&odbc_list
, class, list
) {
621 if (!strcmp(class->name
, cat
)) {
630 new = ast_calloc(1, sizeof(*new));
639 ast_copy_string(new->name
, cat
, sizeof(new->name
));
641 ast_copy_string(new->dsn
, dsn
, sizeof(new->dsn
));
643 ast_copy_string(new->username
, username
, sizeof(new->username
));
645 ast_copy_string(new->password
, password
, sizeof(new->password
));
648 SQLAllocHandle(SQL_HANDLE_ENV
, SQL_NULL_HANDLE
, &new->env
);
649 res
= SQLSetEnvAttr(new->env
, SQL_ATTR_ODBC_VERSION
, (void *) SQL_OV_ODBC3
, 0);
651 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
652 ast_log(LOG_WARNING
, "res_odbc: Error SetEnv\n");
653 SQLFreeHandle(SQL_HANDLE_ENV
, new->env
);
654 AST_LIST_UNLOCK(&odbc_list
);
660 new->haspool
= pooling
;
664 ast_log(LOG_WARNING
, "Pooling without also setting a limit is pointless. Changing limit from 0 to 5.\n");
669 new->backslash_is_escape
= bse
;
670 new->idlecheck
= idlecheck
;
673 ast_log(LOG_NOTICE
, "Refreshing ODBC class '%s' dsn->[%s]\n", cat
, dsn
);
675 odbc_register_class(new, connect
);
676 ast_log(LOG_NOTICE
, "Registered ODBC class '%s' dsn->[%s]\n", cat
, dsn
);
681 ast_config_destroy(config
);
684 /* Purge classes that we know can go away (pooled with 0, only) */
685 AST_LIST_TRAVERSE_SAFE_BEGIN(&odbc_list
, class, list
) {
686 if (class->delme
&& class->haspool
&& class->count
== 0) {
687 AST_LIST_TRAVERSE_SAFE_BEGIN(&(class->odbc_obj
), current
, list
) {
688 AST_LIST_REMOVE_CURRENT(&(class->odbc_obj
), list
);
689 odbc_obj_disconnect(current
);
690 ast_mutex_destroy(¤t
->lock
);
693 AST_LIST_TRAVERSE_SAFE_END
;
695 AST_LIST_REMOVE_CURRENT(&odbc_list
, list
);
699 AST_LIST_TRAVERSE_SAFE_END
;
700 AST_LIST_UNLOCK(&odbc_list
);
705 static int unload_module(void)
707 /* Prohibit unloading */
711 static int load_module(void)
713 if(load_odbc_config() == -1)
714 return AST_MODULE_LOAD_DECLINE
;
715 ast_cli_register_multiple(cli_odbc
, sizeof(cli_odbc
) / sizeof(struct ast_cli_entry
));
716 ast_log(LOG_NOTICE
, "res_odbc loaded.\n");
720 AST_MODULE_INFO(ASTERISK_GPL_KEY
, AST_MODFLAG_GLOBAL_SYMBOLS
, "ODBC Resource",
722 .unload
= unload_module
,