source: branches/samba-3.5.x/nsswitch/libwbclient/wbc_async.c

Last change on this file was 593, checked in by Herwig Bauernfeind, 14 years ago

Samba 3.5: Update trunk to 3.5.7

File size: 18.4 KB
Line 
1/*
2 Unix SMB/CIFS implementation.
3 Infrastructure for async winbind requests
4 Copyright (C) Volker Lendecke 2008
5
6 ** NOTE! The following LGPL license applies to the wbclient
7 ** library. This does NOT imply that all of Samba is released
8 ** under the LGPL
9
10 This library is free software; you can redistribute it and/or
11 modify it under the terms of the GNU Lesser General Public
12 License as published by the Free Software Foundation; either
13 version 3 of the License, or (at your option) any later version.
14
15 This library is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 Library General Public License for more details.
19
20 You should have received a copy of the GNU Lesser General Public License
21 along with this program. If not, see <http://www.gnu.org/licenses/>.
22*/
23
24#include "replace.h"
25#include "system/filesys.h"
26#include "system/network.h"
27#include <talloc.h>
28#include <tevent.h>
29#include "lib/async_req/async_sock.h"
30#include "nsswitch/winbind_struct_protocol.h"
31#include "nsswitch/libwbclient/wbclient.h"
32#include "nsswitch/libwbclient/wbc_async.h"
33
34wbcErr map_wbc_err_from_errno(int error)
35{
36 switch(error) {
37 case EPERM:
38 case EACCES:
39 return WBC_ERR_AUTH_ERROR;
40 case ENOMEM:
41 return WBC_ERR_NO_MEMORY;
42 case EIO:
43 default:
44 return WBC_ERR_UNKNOWN_FAILURE;
45 }
46}
47
48bool tevent_req_is_wbcerr(struct tevent_req *req, wbcErr *pwbc_err)
49{
50 enum tevent_req_state state;
51 uint64_t error;
52 if (!tevent_req_is_error(req, &state, &error)) {
53 *pwbc_err = WBC_ERR_SUCCESS;
54 return false;
55 }
56
57 switch (state) {
58 case TEVENT_REQ_USER_ERROR:
59 *pwbc_err = error;
60 break;
61 case TEVENT_REQ_TIMED_OUT:
62 *pwbc_err = WBC_ERR_UNKNOWN_FAILURE;
63 break;
64 case TEVENT_REQ_NO_MEMORY:
65 *pwbc_err = WBC_ERR_NO_MEMORY;
66 break;
67 default:
68 *pwbc_err = WBC_ERR_UNKNOWN_FAILURE;
69 break;
70 }
71 return true;
72}
73
74wbcErr tevent_req_simple_recv_wbcerr(struct tevent_req *req)
75{
76 wbcErr wbc_err;
77
78 if (tevent_req_is_wbcerr(req, &wbc_err)) {
79 return wbc_err;
80 }
81
82 return WBC_ERR_SUCCESS;
83}
84
85struct wbc_debug_ops {
86 void (*debug)(void *context, enum wbcDebugLevel level,
87 const char *fmt, va_list ap) PRINTF_ATTRIBUTE(3,0);
88 void *context;
89};
90
91struct wb_context {
92 struct tevent_queue *queue;
93 int fd;
94 bool is_priv;
95 const char *dir;
96 struct wbc_debug_ops debug_ops;
97};
98
99static int make_nonstd_fd(int fd)
100{
101 int i;
102 int sys_errno = 0;
103 int fds[3];
104 int num_fds = 0;
105
106 if (fd == -1) {
107 return -1;
108 }
109 while (fd < 3) {
110 fds[num_fds++] = fd;
111 fd = dup(fd);
112 if (fd == -1) {
113 sys_errno = errno;
114 break;
115 }
116 }
117 for (i=0; i<num_fds; i++) {
118 close(fds[i]);
119 }
120 if (fd == -1) {
121 errno = sys_errno;
122 }
123 return fd;
124}
125
126/****************************************************************************
127 Set a fd into blocking/nonblocking mode. Uses POSIX O_NONBLOCK if available,
128 else
129 if SYSV use O_NDELAY
130 if BSD use FNDELAY
131 Set close on exec also.
132****************************************************************************/
133
134static int make_safe_fd(int fd)
135{
136 int result, flags;
137 int new_fd = make_nonstd_fd(fd);
138
139 if (new_fd == -1) {
140 goto fail;
141 }
142
143 /* Socket should be nonblocking. */
144
145#ifdef O_NONBLOCK
146#define FLAG_TO_SET O_NONBLOCK
147#else
148#ifdef SYSV
149#define FLAG_TO_SET O_NDELAY
150#else /* BSD */
151#define FLAG_TO_SET FNDELAY
152#endif
153#endif
154
155 if ((flags = fcntl(new_fd, F_GETFL)) == -1) {
156 goto fail;
157 }
158
159 flags |= FLAG_TO_SET;
160 if (fcntl(new_fd, F_SETFL, flags) == -1) {
161 goto fail;
162 }
163
164#undef FLAG_TO_SET
165
166 /* Socket should be closed on exec() */
167#ifdef FD_CLOEXEC
168 result = flags = fcntl(new_fd, F_GETFD, 0);
169 if (flags >= 0) {
170 flags |= FD_CLOEXEC;
171 result = fcntl( new_fd, F_SETFD, flags );
172 }
173 if (result < 0) {
174 goto fail;
175 }
176#endif
177 return new_fd;
178
179 fail:
180 if (new_fd != -1) {
181 int sys_errno = errno;
182 close(new_fd);
183 errno = sys_errno;
184 }
185 return -1;
186}
187
188/* Just put a prototype to avoid moving the whole function around */
189static const char *winbindd_socket_dir(void);
190
191struct wb_context *wb_context_init(TALLOC_CTX *mem_ctx, const char* dir)
192{
193 struct wb_context *result;
194
195 result = talloc(mem_ctx, struct wb_context);
196 if (result == NULL) {
197 return NULL;
198 }
199 result->queue = tevent_queue_create(result, "wb_trans");
200 if (result->queue == NULL) {
201 TALLOC_FREE(result);
202 return NULL;
203 }
204 result->fd = -1;
205 result->is_priv = false;
206
207 if (dir != NULL) {
208 result->dir = talloc_strdup(result, dir);
209 } else {
210 result->dir = winbindd_socket_dir();
211 }
212 if (result->dir == NULL) {
213 TALLOC_FREE(result);
214 return NULL;
215 }
216 return result;
217}
218
219struct wb_connect_state {
220 int dummy;
221};
222
223static void wbc_connect_connected(struct tevent_req *subreq);
224
225static struct tevent_req *wb_connect_send(TALLOC_CTX *mem_ctx,
226 struct tevent_context *ev,
227 struct wb_context *wb_ctx,
228 const char *dir)
229{
230 struct tevent_req *result, *subreq;
231 struct wb_connect_state *state;
232 struct sockaddr_un sunaddr;
233 struct stat st;
234 char *path = NULL;
235 wbcErr wbc_err;
236
237 result = tevent_req_create(mem_ctx, &state, struct wb_connect_state);
238 if (result == NULL) {
239 return NULL;
240 }
241
242 if (wb_ctx->fd != -1) {
243 close(wb_ctx->fd);
244 wb_ctx->fd = -1;
245 }
246
247 /* Check permissions on unix socket directory */
248
249 if (lstat(dir, &st) == -1) {
250 wbc_err = WBC_ERR_WINBIND_NOT_AVAILABLE;
251 goto post_status;
252 }
253
254 if (!S_ISDIR(st.st_mode) ||
255 (st.st_uid != 0 && st.st_uid != geteuid())) {
256 wbc_err = WBC_ERR_WINBIND_NOT_AVAILABLE;
257 goto post_status;
258 }
259
260 /* Connect to socket */
261
262 path = talloc_asprintf(mem_ctx, "%s/%s", dir,
263 WINBINDD_SOCKET_NAME);
264 if (path == NULL) {
265 goto nomem;
266 }
267
268 sunaddr.sun_family = AF_UNIX;
269 strlcpy(sunaddr.sun_path, path, sizeof(sunaddr.sun_path));
270 TALLOC_FREE(path);
271
272 /* If socket file doesn't exist, don't bother trying to connect
273 with retry. This is an attempt to make the system usable when
274 the winbindd daemon is not running. */
275
276 if ((lstat(sunaddr.sun_path, &st) == -1)
277 || !S_ISSOCK(st.st_mode)
278 || (st.st_uid != 0 && st.st_uid != geteuid())) {
279 wbc_err = WBC_ERR_WINBIND_NOT_AVAILABLE;
280 goto post_status;
281 }
282
283 wb_ctx->fd = make_safe_fd(socket(AF_UNIX, SOCK_STREAM, 0));
284 if (wb_ctx->fd == -1) {
285 wbc_err = map_wbc_err_from_errno(errno);
286 goto post_status;
287 }
288
289 subreq = async_connect_send(mem_ctx, ev, wb_ctx->fd,
290 (struct sockaddr *)(void *)&sunaddr,
291 sizeof(sunaddr));
292 if (subreq == NULL) {
293 goto nomem;
294 }
295 tevent_req_set_callback(subreq, wbc_connect_connected, result);
296 return result;
297
298 post_status:
299 tevent_req_error(result, wbc_err);
300 return tevent_req_post(result, ev);
301 nomem:
302 TALLOC_FREE(result);
303 return NULL;
304}
305
306static void wbc_connect_connected(struct tevent_req *subreq)
307{
308 struct tevent_req *req = tevent_req_callback_data(
309 subreq, struct tevent_req);
310 int res, err;
311
312 res = async_connect_recv(subreq, &err);
313 TALLOC_FREE(subreq);
314 if (res == -1) {
315 tevent_req_error(req, map_wbc_err_from_errno(err));
316 return;
317 }
318 tevent_req_done(req);
319}
320
321static wbcErr wb_connect_recv(struct tevent_req *req)
322{
323 return tevent_req_simple_recv_wbcerr(req);
324}
325
326static const char *winbindd_socket_dir(void)
327{
328#ifdef SOCKET_WRAPPER
329 const char *env_dir;
330
331 env_dir = getenv(WINBINDD_SOCKET_DIR_ENVVAR);
332 if (env_dir) {
333 return env_dir;
334 }
335#endif
336
337 return WINBINDD_SOCKET_DIR;
338}
339
340struct wb_open_pipe_state {
341 struct wb_context *wb_ctx;
342 struct tevent_context *ev;
343 bool need_priv;
344 struct winbindd_request wb_req;
345};
346
347static void wb_open_pipe_connect_nonpriv_done(struct tevent_req *subreq);
348static void wb_open_pipe_ping_done(struct tevent_req *subreq);
349static void wb_open_pipe_getpriv_done(struct tevent_req *subreq);
350static void wb_open_pipe_connect_priv_done(struct tevent_req *subreq);
351
352static struct tevent_req *wb_open_pipe_send(TALLOC_CTX *mem_ctx,
353 struct tevent_context *ev,
354 struct wb_context *wb_ctx,
355 bool need_priv)
356{
357 struct tevent_req *result, *subreq;
358 struct wb_open_pipe_state *state;
359
360 result = tevent_req_create(mem_ctx, &state, struct wb_open_pipe_state);
361 if (result == NULL) {
362 return NULL;
363 }
364 state->wb_ctx = wb_ctx;
365 state->ev = ev;
366 state->need_priv = need_priv;
367
368 if (wb_ctx->fd != -1) {
369 close(wb_ctx->fd);
370 wb_ctx->fd = -1;
371 }
372
373 subreq = wb_connect_send(state, ev, wb_ctx, wb_ctx->dir);
374 if (subreq == NULL) {
375 goto fail;
376 }
377 tevent_req_set_callback(subreq, wb_open_pipe_connect_nonpriv_done,
378 result);
379 return result;
380
381 fail:
382 TALLOC_FREE(result);
383 return NULL;
384}
385
386static void wb_open_pipe_connect_nonpriv_done(struct tevent_req *subreq)
387{
388 struct tevent_req *req = tevent_req_callback_data(
389 subreq, struct tevent_req);
390 struct wb_open_pipe_state *state = tevent_req_data(
391 req, struct wb_open_pipe_state);
392 wbcErr wbc_err;
393
394 wbc_err = wb_connect_recv(subreq);
395 TALLOC_FREE(subreq);
396 if (!WBC_ERROR_IS_OK(wbc_err)) {
397 state->wb_ctx->is_priv = true;
398 tevent_req_error(req, wbc_err);
399 return;
400 }
401
402 ZERO_STRUCT(state->wb_req);
403 state->wb_req.cmd = WINBINDD_INTERFACE_VERSION;
404 state->wb_req.pid = getpid();
405
406 subreq = wb_simple_trans_send(state, state->ev, NULL,
407 state->wb_ctx->fd, &state->wb_req);
408 if (tevent_req_nomem(subreq, req)) {
409 return;
410 }
411 tevent_req_set_callback(subreq, wb_open_pipe_ping_done, req);
412}
413
414static void wb_open_pipe_ping_done(struct tevent_req *subreq)
415{
416 struct tevent_req *req = tevent_req_callback_data(
417 subreq, struct tevent_req);
418 struct wb_open_pipe_state *state = tevent_req_data(
419 req, struct wb_open_pipe_state);
420 struct winbindd_response *wb_resp;
421 int ret, err;
422
423 ret = wb_simple_trans_recv(subreq, state, &wb_resp, &err);
424 TALLOC_FREE(subreq);
425 if (ret == -1) {
426 tevent_req_error(req, map_wbc_err_from_errno(err));
427 return;
428 }
429
430 if (!state->need_priv) {
431 tevent_req_done(req);
432 return;
433 }
434
435 state->wb_req.cmd = WINBINDD_PRIV_PIPE_DIR;
436 state->wb_req.pid = getpid();
437
438 subreq = wb_simple_trans_send(state, state->ev, NULL,
439 state->wb_ctx->fd, &state->wb_req);
440 if (tevent_req_nomem(subreq, req)) {
441 return;
442 }
443 tevent_req_set_callback(subreq, wb_open_pipe_getpriv_done, req);
444}
445
446static void wb_open_pipe_getpriv_done(struct tevent_req *subreq)
447{
448 struct tevent_req *req = tevent_req_callback_data(
449 subreq, struct tevent_req);
450 struct wb_open_pipe_state *state = tevent_req_data(
451 req, struct wb_open_pipe_state);
452 struct winbindd_response *wb_resp = NULL;
453 int ret, err;
454
455 ret = wb_simple_trans_recv(subreq, state, &wb_resp, &err);
456 TALLOC_FREE(subreq);
457 if (ret == -1) {
458 tevent_req_error(req, map_wbc_err_from_errno(err));
459 return;
460 }
461
462 close(state->wb_ctx->fd);
463 state->wb_ctx->fd = -1;
464
465 subreq = wb_connect_send(state, state->ev, state->wb_ctx,
466 (char *)wb_resp->extra_data.data);
467 TALLOC_FREE(wb_resp);
468 if (tevent_req_nomem(subreq, req)) {
469 return;
470 }
471 tevent_req_set_callback(subreq, wb_open_pipe_connect_priv_done, req);
472}
473
474static void wb_open_pipe_connect_priv_done(struct tevent_req *subreq)
475{
476 struct tevent_req *req = tevent_req_callback_data(
477 subreq, struct tevent_req);
478 struct wb_open_pipe_state *state = tevent_req_data(
479 req, struct wb_open_pipe_state);
480 wbcErr wbc_err;
481
482 wbc_err = wb_connect_recv(subreq);
483 TALLOC_FREE(subreq);
484 if (!WBC_ERROR_IS_OK(wbc_err)) {
485 tevent_req_error(req, wbc_err);
486 return;
487 }
488 state->wb_ctx->is_priv = true;
489 tevent_req_done(req);
490}
491
492static wbcErr wb_open_pipe_recv(struct tevent_req *req)
493{
494 return tevent_req_simple_recv_wbcerr(req);
495}
496
497struct wb_trans_state {
498 struct wb_trans_state *prev, *next;
499 struct wb_context *wb_ctx;
500 struct tevent_context *ev;
501 struct winbindd_request *wb_req;
502 struct winbindd_response *wb_resp;
503 bool need_priv;
504};
505
506static bool closed_fd(int fd)
507{
508 struct timeval tv;
509 fd_set r_fds;
510 int selret;
511
512 if (fd < 0 || fd >= FD_SETSIZE) {
513 return true;
514 }
515
516 FD_ZERO(&r_fds);
517 FD_SET(fd, &r_fds);
518 ZERO_STRUCT(tv);
519
520 selret = select(fd+1, &r_fds, NULL, NULL, &tv);
521 if (selret == -1) {
522 return true;
523 }
524 if (selret == 0) {
525 return false;
526 }
527 return (FD_ISSET(fd, &r_fds));
528}
529
530static void wb_trans_trigger(struct tevent_req *req, void *private_data);
531static void wb_trans_connect_done(struct tevent_req *subreq);
532static void wb_trans_done(struct tevent_req *subreq);
533static void wb_trans_retry_wait_done(struct tevent_req *subreq);
534
535struct tevent_req *wb_trans_send(TALLOC_CTX *mem_ctx,
536 struct tevent_context *ev,
537 struct wb_context *wb_ctx, bool need_priv,
538 struct winbindd_request *wb_req)
539{
540 struct tevent_req *req;
541 struct wb_trans_state *state;
542
543 req = tevent_req_create(mem_ctx, &state, struct wb_trans_state);
544 if (req == NULL) {
545 return NULL;
546 }
547 state->wb_ctx = wb_ctx;
548 state->ev = ev;
549 state->wb_req = wb_req;
550 state->need_priv = need_priv;
551
552 if (!tevent_queue_add(wb_ctx->queue, ev, req, wb_trans_trigger,
553 NULL)) {
554 tevent_req_nomem(NULL, req);
555 return tevent_req_post(req, ev);
556 }
557 return req;
558}
559
560static void wb_trans_trigger(struct tevent_req *req, void *private_data)
561{
562 struct wb_trans_state *state = tevent_req_data(
563 req, struct wb_trans_state);
564 struct tevent_req *subreq;
565
566 if ((state->wb_ctx->fd != -1) && closed_fd(state->wb_ctx->fd)) {
567 close(state->wb_ctx->fd);
568 state->wb_ctx->fd = -1;
569 }
570
571 if ((state->wb_ctx->fd == -1)
572 || (state->need_priv && !state->wb_ctx->is_priv)) {
573 subreq = wb_open_pipe_send(state, state->ev, state->wb_ctx,
574 state->need_priv);
575 if (tevent_req_nomem(subreq, req)) {
576 return;
577 }
578 tevent_req_set_callback(subreq, wb_trans_connect_done, req);
579 return;
580 }
581
582 state->wb_req->pid = getpid();
583
584 subreq = wb_simple_trans_send(state, state->ev, NULL,
585 state->wb_ctx->fd, state->wb_req);
586 if (tevent_req_nomem(subreq, req)) {
587 return;
588 }
589 tevent_req_set_callback(subreq, wb_trans_done, req);
590}
591
592static bool wb_trans_retry(struct tevent_req *req,
593 struct wb_trans_state *state,
594 wbcErr wbc_err)
595{
596 struct tevent_req *subreq;
597
598 if (WBC_ERROR_IS_OK(wbc_err)) {
599 return false;
600 }
601
602 if (wbc_err == WBC_ERR_WINBIND_NOT_AVAILABLE) {
603 /*
604 * Winbind not around or we can't connect to the pipe. Fail
605 * immediately.
606 */
607 tevent_req_error(req, wbc_err);
608 return true;
609 }
610
611 /*
612 * The transfer as such failed, retry after one second
613 */
614
615 if (state->wb_ctx->fd != -1) {
616 close(state->wb_ctx->fd);
617 state->wb_ctx->fd = -1;
618 }
619
620 subreq = tevent_wakeup_send(state, state->ev,
621 tevent_timeval_current_ofs(1, 0));
622 if (tevent_req_nomem(subreq, req)) {
623 return true;
624 }
625 tevent_req_set_callback(subreq, wb_trans_retry_wait_done, req);
626 return true;
627}
628
629static void wb_trans_retry_wait_done(struct tevent_req *subreq)
630{
631 struct tevent_req *req = tevent_req_callback_data(
632 subreq, struct tevent_req);
633 struct wb_trans_state *state = tevent_req_data(
634 req, struct wb_trans_state);
635 bool ret;
636
637 ret = tevent_wakeup_recv(subreq);
638 TALLOC_FREE(subreq);
639 if (!ret) {
640 tevent_req_error(req, WBC_ERR_UNKNOWN_FAILURE);
641 return;
642 }
643
644 subreq = wb_open_pipe_send(state, state->ev, state->wb_ctx,
645 state->need_priv);
646 if (tevent_req_nomem(subreq, req)) {
647 return;
648 }
649 tevent_req_set_callback(subreq, wb_trans_connect_done, req);
650}
651
652static void wb_trans_connect_done(struct tevent_req *subreq)
653{
654 struct tevent_req *req = tevent_req_callback_data(
655 subreq, struct tevent_req);
656 struct wb_trans_state *state = tevent_req_data(
657 req, struct wb_trans_state);
658 wbcErr wbc_err;
659
660 wbc_err = wb_open_pipe_recv(subreq);
661 TALLOC_FREE(subreq);
662
663 if (wb_trans_retry(req, state, wbc_err)) {
664 return;
665 }
666
667 subreq = wb_simple_trans_send(state, state->ev, NULL,
668 state->wb_ctx->fd, state->wb_req);
669 if (tevent_req_nomem(subreq, req)) {
670 return;
671 }
672 tevent_req_set_callback(subreq, wb_trans_done, req);
673}
674
675static void wb_trans_done(struct tevent_req *subreq)
676{
677 struct tevent_req *req = tevent_req_callback_data(
678 subreq, struct tevent_req);
679 struct wb_trans_state *state = tevent_req_data(
680 req, struct wb_trans_state);
681 int ret, err;
682
683 ret = wb_simple_trans_recv(subreq, state, &state->wb_resp, &err);
684 TALLOC_FREE(subreq);
685 if ((ret == -1)
686 && wb_trans_retry(req, state, map_wbc_err_from_errno(err))) {
687 return;
688 }
689
690 tevent_req_done(req);
691}
692
693wbcErr wb_trans_recv(struct tevent_req *req, TALLOC_CTX *mem_ctx,
694 struct winbindd_response **presponse)
695{
696 struct wb_trans_state *state = tevent_req_data(
697 req, struct wb_trans_state);
698 wbcErr wbc_err;
699
700 if (tevent_req_is_wbcerr(req, &wbc_err)) {
701 return wbc_err;
702 }
703
704 *presponse = talloc_move(mem_ctx, &state->wb_resp);
705 return WBC_ERR_SUCCESS;
706}
707
708/********************************************************************
709 * Debug wrapper functions, modeled (with lot's of code copied as is)
710 * after the tevent debug wrapper functions
711 ********************************************************************/
712
713/*
714 this allows the user to choose their own debug function
715*/
716int wbcSetDebug(struct wb_context *wb_ctx,
717 void (*debug)(void *context,
718 enum wbcDebugLevel level,
719 const char *fmt,
720 va_list ap) PRINTF_ATTRIBUTE(3,0),
721 void *context)
722{
723 wb_ctx->debug_ops.debug = debug;
724 wb_ctx->debug_ops.context = context;
725 return 0;
726}
727
728/*
729 debug function for wbcSetDebugStderr
730*/
731static void wbcDebugStderr(void *private_data,
732 enum wbcDebugLevel level,
733 const char *fmt,
734 va_list ap) PRINTF_ATTRIBUTE(3,0);
735static void wbcDebugStderr(void *private_data,
736 enum wbcDebugLevel level,
737 const char *fmt, va_list ap)
738{
739 if (level <= WBC_DEBUG_WARNING) {
740 vfprintf(stderr, fmt, ap);
741 }
742}
743
744/*
745 convenience function to setup debug messages on stderr
746 messages of level WBC_DEBUG_WARNING and higher are printed
747*/
748int wbcSetDebugStderr(struct wb_context *wb_ctx)
749{
750 return wbcSetDebug(wb_ctx, wbcDebugStderr, wb_ctx);
751}
752
753/*
754 * log a message
755 *
756 * The default debug action is to ignore debugging messages.
757 * This is the most appropriate action for a library.
758 * Applications using the library must decide where to
759 * redirect debugging messages
760*/
761void wbcDebug(struct wb_context *wb_ctx, enum wbcDebugLevel level,
762 const char *fmt, ...)
763{
764 va_list ap;
765 if (!wb_ctx) {
766 return;
767 }
768 if (wb_ctx->debug_ops.debug == NULL) {
769 return;
770 }
771 va_start(ap, fmt);
772 wb_ctx->debug_ops.debug(wb_ctx->debug_ops.context, level, fmt, ap);
773 va_end(ap);
774}
Note: See TracBrowser for help on using the repository browser.