blob: dce4534c166daa1a954e117905aa2f186b0af87a [file] [log] [blame]
Alexandre Lision8af73cb2013-12-10 14:11:20 -05001/* $Id: sipecho.c 4537 2013-06-19 06:47:43Z riza $ */
Tristan Matthews0a329cc2013-07-17 13:20:14 -04002/*
3 * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
4 * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 */
20
21
22/**
23 * sipecho.c
24 *
25 * - Accepts incoming calls and echoes back SDP and any media.
26 * - Specify URI in cmdline argument to make call
27 * - Accepts registration too!
28 */
29
30/* Include all headers. */
31#include <pjsip.h>
32#include <pjmedia/sdp.h>
33#include <pjsip_ua.h>
34#include <pjlib-util.h>
35#include <pjlib.h>
36
37/* For logging purpose. */
38#define THIS_FILE "sipecho.c"
39
40#include "util.h"
41
42
43/* Settings */
44#define MAX_CALLS 8
45
46typedef struct call_t
47{
48 pjsip_inv_session *inv;
49} call_t;
50
51static struct app_t
52{
53 pj_caching_pool cp;
54 pj_pool_t *pool;
55
56 pjsip_endpoint *sip_endpt;
57 //pjmedia_endpt *med_endpt;
58
59 call_t call[MAX_CALLS];
60
61 pj_bool_t quit;
62 pj_thread_t *worker_thread;
63
64 pj_bool_t enable_msg_logging;
65} app;
66
67/*
68 * Prototypes:
69 */
70
71static void call_on_media_update(pjsip_inv_session *inv, pj_status_t status);
72static void call_on_state_changed(pjsip_inv_session *inv, pjsip_event *e);
73static void call_on_rx_offer(pjsip_inv_session *inv, const pjmedia_sdp_session *offer);
74static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e);
75static pj_bool_t on_rx_request( pjsip_rx_data *rdata );
76
77/* Globals */
78static int sip_af;
79static int sip_port = 5060;
80static pj_bool_t sip_tcp;
81
82/* This is a PJSIP module to be registered by application to handle
83 * incoming requests outside any dialogs/transactions. The main purpose
84 * here is to handle incoming INVITE request message, where we will
85 * create a dialog and INVITE session for it.
86 */
87static pjsip_module mod_sipecho =
88{
89 NULL, NULL, /* prev, next. */
90 { "mod-sipecho", 11 }, /* Name. */
91 -1, /* Id */
92 PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */
93 NULL, /* load() */
94 NULL, /* start() */
95 NULL, /* stop() */
96 NULL, /* unload() */
97 &on_rx_request, /* on_rx_request() */
98 NULL, /* on_rx_response() */
99 NULL, /* on_tx_request. */
100 NULL, /* on_tx_response() */
101 NULL, /* on_tsx_state() */
102};
103
104/* Notification on incoming messages */
105static pj_bool_t logging_on_rx_msg(pjsip_rx_data *rdata)
106{
107 if (!app.enable_msg_logging)
108 return PJ_FALSE;
109
110 PJ_LOG(3,(THIS_FILE, "RX %d bytes %s from %s %s:%d:\n"
111 "%.*s\n"
112 "--end msg--",
113 rdata->msg_info.len,
114 pjsip_rx_data_get_info(rdata),
115 rdata->tp_info.transport->type_name,
116 rdata->pkt_info.src_name,
117 rdata->pkt_info.src_port,
118 (int)rdata->msg_info.len,
119 rdata->msg_info.msg_buf));
120 return PJ_FALSE;
121}
122
123/* Notification on outgoing messages */
124static pj_status_t logging_on_tx_msg(pjsip_tx_data *tdata)
125{
126 if (!app.enable_msg_logging)
127 return PJ_SUCCESS;
128
129 PJ_LOG(3,(THIS_FILE, "TX %d bytes %s to %s %s:%d:\n"
130 "%.*s\n"
131 "--end msg--",
132 (tdata->buf.cur - tdata->buf.start),
133 pjsip_tx_data_get_info(tdata),
134 tdata->tp_info.transport->type_name,
135 tdata->tp_info.dst_name,
136 tdata->tp_info.dst_port,
137 (int)(tdata->buf.cur - tdata->buf.start),
138 tdata->buf.start));
139 return PJ_SUCCESS;
140}
141
142/* The module instance. */
143static pjsip_module msg_logger =
144{
145 NULL, NULL, /* prev, next. */
146 { "mod-msg-log", 13 }, /* Name. */
147 -1, /* Id */
148 PJSIP_MOD_PRIORITY_TRANSPORT_LAYER-1,/* Priority */
149 NULL, /* load() */
150 NULL, /* start() */
151 NULL, /* stop() */
152 NULL, /* unload() */
153 &logging_on_rx_msg, /* on_rx_request() */
154 &logging_on_rx_msg, /* on_rx_response() */
155 &logging_on_tx_msg, /* on_tx_request. */
156 &logging_on_tx_msg, /* on_tx_response() */
157 NULL, /* on_tsx_state() */
158
159};
160
161static int worker_proc(void *arg)
162{
163 PJ_UNUSED_ARG(arg);
164
165 while (!app.quit) {
166 pj_time_val interval = { 0, 20 };
167 pjsip_endpt_handle_events(app.sip_endpt, &interval);
168 }
169
170 return 0;
171}
172
173static void hangup_all(void)
174{
175 unsigned i;
176 for (i=0; i<MAX_CALLS; ++i) {
177 call_t *call = &app.call[i];
178
179 if (call->inv && call->inv->state <= PJSIP_INV_STATE_CONFIRMED) {
180 pj_status_t status;
181 pjsip_tx_data *tdata;
182
183 status = pjsip_inv_end_session(call->inv, PJSIP_SC_BUSY_HERE, NULL, &tdata);
184 if (status==PJ_SUCCESS && tdata)
185 pjsip_inv_send_msg(call->inv, tdata);
186 }
187 }
188}
189
190static void destroy_stack(void)
191{
192 enum { WAIT_CLEAR = 5000, WAIT_INTERVAL = 500 };
193 unsigned i;
194
195 PJ_LOG(3,(THIS_FILE, "Shutting down.."));
196
197 /* Wait until all clear */
198 hangup_all();
199 for (i=0; i<WAIT_CLEAR/WAIT_INTERVAL; ++i) {
200 unsigned j;
201
202 for (j=0; j<MAX_CALLS; ++j) {
203 call_t *call = &app.call[j];
204 if (call->inv && call->inv->state <= PJSIP_INV_STATE_CONFIRMED)
205 break;
206 }
207
208 if (j==MAX_CALLS)
209 return;
210
211 pj_thread_sleep(WAIT_INTERVAL);
212 }
213
214 app.quit = PJ_TRUE;
215 if (app.worker_thread) {
216 pj_thread_join(app.worker_thread);
217 app.worker_thread = NULL;
218 }
219
220 //if (app.med_endpt)
221 //pjmedia_endpt_destroy(app.med_endpt);
222
223 if (app.sip_endpt)
224 pjsip_endpt_destroy(app.sip_endpt);
225
226 if (app.pool)
227 pj_pool_release(app.pool);
228
229 dump_pool_usage(THIS_FILE, &app.cp);
230 pj_caching_pool_destroy(&app.cp);
231}
232
233#define CHECK_STATUS() do { if (status != PJ_SUCCESS) return status; } while (0)
234
235static pj_status_t init_stack()
236{
237 pj_sockaddr addr;
238 pjsip_inv_callback inv_cb;
239 pj_status_t status;
240
241 pj_log_set_level(3);
242
243 status = pjlib_util_init();
244 CHECK_STATUS();
245
246 pj_caching_pool_init(&app.cp, NULL, 0);
247 app.pool = pj_pool_create( &app.cp.factory, "sipecho", 512, 512, 0);
248
249 status = pjsip_endpt_create(&app.cp.factory, NULL, &app.sip_endpt);
250 CHECK_STATUS();
251
252 pj_log_set_level(4);
253 pj_sockaddr_init((pj_uint16_t)sip_af, &addr, NULL, (pj_uint16_t)sip_port);
254 if (sip_af == pj_AF_INET()) {
255 if (sip_tcp) {
256 status = pjsip_tcp_transport_start( app.sip_endpt, &addr.ipv4, 1,
257 NULL);
258 } else {
259 status = pjsip_udp_transport_start( app.sip_endpt, &addr.ipv4,
260 NULL, 1, NULL);
261 }
262 } else if (sip_af == pj_AF_INET6()) {
263 status = pjsip_udp_transport_start6(app.sip_endpt, &addr.ipv6,
264 NULL, 1, NULL);
265 } else {
266 status = PJ_EAFNOTSUP;
267 }
268
269 pj_log_set_level(3);
270 CHECK_STATUS();
271
272 status = pjsip_tsx_layer_init_module(app.sip_endpt) ||
273 pjsip_ua_init_module( app.sip_endpt, NULL );
274 CHECK_STATUS();
275
276 pj_bzero(&inv_cb, sizeof(inv_cb));
277 inv_cb.on_state_changed = &call_on_state_changed;
278 inv_cb.on_new_session = &call_on_forked;
279 inv_cb.on_media_update = &call_on_media_update;
280 inv_cb.on_rx_offer = &call_on_rx_offer;
281
282 status = pjsip_inv_usage_init(app.sip_endpt, &inv_cb) ||
283 pjsip_100rel_init_module(app.sip_endpt) ||
284 pjsip_endpt_register_module( app.sip_endpt, &mod_sipecho) ||
285 pjsip_endpt_register_module( app.sip_endpt, &msg_logger) ||
286 //pjmedia_endpt_create(&app.cp.factory,
287 // pjsip_endpt_get_ioqueue(app.sip_endpt),
288 // 0, &app.med_endpt) ||
289 pj_thread_create(app.pool, "sipecho", &worker_proc, NULL, 0, 0,
290 &app.worker_thread);
291 CHECK_STATUS();
292
293 return PJ_SUCCESS;
294}
295
296static void destroy_call(call_t *call)
297{
298 call->inv = NULL;
299}
300
301static pjmedia_sdp_attr * find_remove_sdp_attrs(unsigned *cnt,
302 pjmedia_sdp_attr *attr[],
303 unsigned cnt_attr_to_remove,
304 const char* attr_to_remove[])
305{
306 pjmedia_sdp_attr *found_attr = NULL;
307 int i;
308
309 for (i=0; i<(int)*cnt; ++i) {
310 unsigned j;
311 for (j=0; j<cnt_attr_to_remove; ++j) {
312 if (pj_strcmp2(&attr[i]->name, attr_to_remove[j])==0) {
313 if (!found_attr) found_attr = attr[i];
314 pj_array_erase(attr, sizeof(attr[0]), *cnt, i);
315 --(*cnt);
316 --i;
317 break;
318 }
319 }
320 }
321
322 return found_attr;
323}
324
325static pjmedia_sdp_session *create_answer(int call_num, pj_pool_t *pool,
326 const pjmedia_sdp_session *offer)
327{
328 const char* dir_attrs[] = { "sendrecv", "sendonly", "recvonly", "inactive" };
329 const char *ice_attrs[] = {"ice-pwd", "ice-ufrag", "candidate"};
330 pjmedia_sdp_session *answer = pjmedia_sdp_session_clone(pool, offer);
331 pjmedia_sdp_attr *sess_dir_attr = NULL;
332 unsigned mi;
333
334 PJ_LOG(3,(THIS_FILE, "Call %d: creating answer:", call_num));
335
336 answer->name = pj_str("sipecho");
337 sess_dir_attr = find_remove_sdp_attrs(&answer->attr_count, answer->attr,
338 PJ_ARRAY_SIZE(dir_attrs),
339 dir_attrs);
340
341 for (mi=0; mi<answer->media_count; ++mi) {
342 pjmedia_sdp_media *m = answer->media[mi];
343 pjmedia_sdp_attr *m_dir_attr;
344 pjmedia_sdp_attr *dir_attr;
345 const char *our_dir = NULL;
346 pjmedia_sdp_conn *c;
347
348 /* Match direction */
349 m_dir_attr = find_remove_sdp_attrs(&m->attr_count, m->attr,
350 PJ_ARRAY_SIZE(dir_attrs),
351 dir_attrs);
352 dir_attr = m_dir_attr ? m_dir_attr : sess_dir_attr;
353
354 if (dir_attr) {
355 if (pj_strcmp2(&dir_attr->name, "sendonly")==0)
356 our_dir = "recvonly";
357 else if (pj_strcmp2(&dir_attr->name, "inactive")==0)
358 our_dir = "inactive";
359 else if (pj_strcmp2(&dir_attr->name, "recvonly")==0)
360 our_dir = "inactive";
361
362 if (our_dir) {
363 dir_attr = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_attr);
364 dir_attr->name = pj_str((char*)our_dir);
365 m->attr[m->attr_count++] = dir_attr;
366 }
367 }
368
369 /* Remove ICE attributes */
370 find_remove_sdp_attrs(&m->attr_count, m->attr, PJ_ARRAY_SIZE(ice_attrs), ice_attrs);
371
372 /* Done */
373 c = m->conn ? m->conn : answer->conn;
374 PJ_LOG(3,(THIS_FILE, " Media %d, %.*s: %s <--> %.*s:%d",
375 mi, (int)m->desc.media.slen, m->desc.media.ptr,
376 (our_dir ? our_dir : "sendrecv"),
377 (int)c->addr.slen, c->addr.ptr, m->desc.port));
378 }
379
380 return answer;
381}
382
383static void call_on_state_changed( pjsip_inv_session *inv,
384 pjsip_event *e)
385{
386 call_t *call = (call_t*)inv->mod_data[mod_sipecho.id];
387 if (!call)
388 return;
389
390 PJ_UNUSED_ARG(e);
391 if (inv->state == PJSIP_INV_STATE_DISCONNECTED) {
392 PJ_LOG(3,(THIS_FILE, "Call %d: DISCONNECTED [reason=%d (%s)]",
393 call - app.call, inv->cause,
394 pjsip_get_status_text(inv->cause)->ptr));
395 destroy_call(call);
396 } else {
397 PJ_LOG(3,(THIS_FILE, "Call %d: state changed to %s",
398 call - app.call, pjsip_inv_state_name(inv->state)));
399 }
400}
401
402static void call_on_rx_offer(pjsip_inv_session *inv, const pjmedia_sdp_session *offer)
403{
404 call_t *call = (call_t*) inv->mod_data[mod_sipecho.id];
405 pjsip_inv_set_sdp_answer(inv, create_answer((int)(call - app.call),
406 inv->pool_prov, offer));
407}
408
409static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e)
410{
411 PJ_UNUSED_ARG(inv);
412 PJ_UNUSED_ARG(e);
413}
414
415static pj_bool_t on_rx_request( pjsip_rx_data *rdata )
416{
417 pj_sockaddr hostaddr;
418 char temp[80], hostip[PJ_INET6_ADDRSTRLEN];
419 pj_str_t local_uri;
420 pjsip_dialog *dlg;
421 pjsip_rdata_sdp_info *sdp_info;
422 pjmedia_sdp_session *answer = NULL;
423 pjsip_tx_data *tdata = NULL;
424 call_t *call = NULL;
425 unsigned i;
426 pj_status_t status;
427
428 PJ_LOG(3,(THIS_FILE, "RX %.*s from %s",
429 (int)rdata->msg_info.msg->line.req.method.name.slen,
430 rdata->msg_info.msg->line.req.method.name.ptr,
431 rdata->pkt_info.src_name));
432
433 if (rdata->msg_info.msg->line.req.method.id == PJSIP_REGISTER_METHOD) {
434 /* Let me be a registrar! */
435 pjsip_hdr hdr_list, *h;
436 pjsip_msg *msg;
437 int expires = -1;
438
439 pj_list_init(&hdr_list);
440 msg = rdata->msg_info.msg;
441 h = (pjsip_hdr*)pjsip_msg_find_hdr(msg, PJSIP_H_EXPIRES, NULL);
442 if (h) {
443 expires = ((pjsip_expires_hdr*)h)->ivalue;
444 pj_list_push_back(&hdr_list, pjsip_hdr_clone(rdata->tp_info.pool, h));
445 PJ_LOG(3,(THIS_FILE, " Expires=%d", expires));
446 }
447 if (expires != 0) {
448 h = (pjsip_hdr*)pjsip_msg_find_hdr(msg, PJSIP_H_CONTACT, NULL);
449 if (h)
450 pj_list_push_back(&hdr_list, pjsip_hdr_clone(rdata->tp_info.pool, h));
451 }
452
453 pjsip_endpt_respond(app.sip_endpt, &mod_sipecho, rdata, 200, NULL,
454 &hdr_list, NULL, NULL);
455 return PJ_TRUE;
456 }
457
458 if (rdata->msg_info.msg->line.req.method.id != PJSIP_INVITE_METHOD) {
459 if (rdata->msg_info.msg->line.req.method.id != PJSIP_ACK_METHOD) {
460 pj_str_t reason = pj_str("Go away");
461 pjsip_endpt_respond_stateless( app.sip_endpt, rdata,
462 400, &reason,
463 NULL, NULL);
464 }
465 return PJ_TRUE;
466 }
467
468 sdp_info = pjsip_rdata_get_sdp_info(rdata);
469 if (!sdp_info || !sdp_info->sdp) {
470 pj_str_t reason = pj_str("Require valid offer");
471 pjsip_endpt_respond_stateless( app.sip_endpt, rdata,
472 400, &reason,
473 NULL, NULL);
474 }
475
476 for (i=0; i<MAX_CALLS; ++i) {
477 if (app.call[i].inv == NULL) {
478 call = &app.call[i];
479 break;
480 }
481 }
482
483 if (i==MAX_CALLS) {
484 pj_str_t reason = pj_str("We're full");
485 pjsip_endpt_respond_stateless( app.sip_endpt, rdata,
486 PJSIP_SC_BUSY_HERE, &reason,
487 NULL, NULL);
488 return PJ_TRUE;
489 }
490
491 /* Generate Contact URI */
492 status = pj_gethostip(sip_af, &hostaddr);
493 if (status != PJ_SUCCESS) {
494 app_perror(THIS_FILE, "Unable to retrieve local host IP", status);
495 return PJ_TRUE;
496 }
497 pj_sockaddr_print(&hostaddr, hostip, sizeof(hostip), 2);
498 pj_ansi_sprintf(temp, "<sip:sipecho@%s:%d>", hostip, sip_port);
499 local_uri = pj_str(temp);
500
501 status = pjsip_dlg_create_uas( pjsip_ua_instance(), rdata,
502 &local_uri, &dlg);
503
504 if (status == PJ_SUCCESS)
505 answer = create_answer((int)(call-app.call), dlg->pool, sdp_info->sdp);
506 if (status == PJ_SUCCESS)
507 status = pjsip_inv_create_uas( dlg, rdata, answer, 0, &call->inv);
508 if (status == PJ_SUCCESS)
509 status = pjsip_inv_initial_answer(call->inv, rdata, 100,
510 NULL, NULL, &tdata);
511 if (status == PJ_SUCCESS)
512 status = pjsip_inv_send_msg(call->inv, tdata);
513
514 if (status == PJ_SUCCESS)
515 status = pjsip_inv_answer(call->inv, 180, NULL,
516 NULL, &tdata);
517 if (status == PJ_SUCCESS)
518 status = pjsip_inv_send_msg(call->inv, tdata);
519
520 if (status == PJ_SUCCESS)
521 status = pjsip_inv_answer(call->inv, 200, NULL,
522 NULL, &tdata);
523 if (status == PJ_SUCCESS)
524 status = pjsip_inv_send_msg(call->inv, tdata);
525
526 if (status != PJ_SUCCESS) {
527 pjsip_endpt_respond_stateless( app.sip_endpt, rdata,
528 500, NULL, NULL, NULL);
529 destroy_call(call);
530 } else {
531 call->inv->mod_data[mod_sipecho.id] = call;
532 }
533
534 return PJ_TRUE;
535}
536
537static void call_on_media_update( pjsip_inv_session *inv,
538 pj_status_t status)
539{
540 PJ_UNUSED_ARG(inv);
541 PJ_UNUSED_ARG(status);
542}
543
544
545static void usage()
546{
547 printf("\nUsage: sipecho OPTIONS\n");
548 printf("\n");
549 printf("where OPTIONS:\n");
550 printf(" --local-port, -p PORT Bind to port PORT.\n");
551 printf(" --tcp, -t Listen to TCP instead.\n");
552 printf(" --ipv6, -6 Use IPv6 instead.\n");
553 printf(" --help, -h Show this help page.\n");
554}
555
556/* main()
557 *
558 * If called with argument, treat argument as SIP URL to be called.
559 * Otherwise wait for incoming calls.
560 */
561int main(int argc, char *argv[])
562{
563 struct pj_getopt_option long_options[] = {
564 { "local-port", 1, 0, 'p' },
565 { "tcp", 0, 0, 't' },
566 { "ipv6", 0, 0, '6' },
567 { "help", 0, 0, 'h' }
568 };
569 int c, option_index;
570
571 pj_log_set_level(5);
572
573 pj_init();
574
575 sip_af = pj_AF_INET();
576
577 pj_optind = 0;
578 while ((c = pj_getopt_long(argc, argv, "p:t6h", long_options,
579 &option_index)) != -1)
580 {
581 switch (c) {
582 case 'p':
583 sip_port = atoi(pj_optarg);
584 break;
585 case 't':
586 sip_tcp = PJ_TRUE;
587 break;
588 case 'h':
589 usage();
590 return 0;
591 case '6':
592 sip_af = pj_AF_INET6();
593 break;
594 default:
595 PJ_LOG(1,(THIS_FILE,
596 "Argument \"%s\" is not valid. Use --help to see help",
597 argv[pj_optind-1]));
598 return -1;
599 }
600 }
601
602 if (init_stack())
603 goto on_error;
604
605 /* If URL is specified, then make call immediately. */
606 if (pj_optind != argc) {
607 pj_sockaddr hostaddr;
608 char hostip[PJ_INET6_ADDRSTRLEN+2];
609 char temp[80];
610 call_t *call;
611 pj_str_t dst_uri = pj_str(argv[pj_optind]);
612 pj_str_t local_uri;
613 pjsip_dialog *dlg;
614 pj_status_t status;
615 pjsip_tx_data *tdata;
616
617 if (pj_gethostip(sip_af, &hostaddr) != PJ_SUCCESS) {
618 PJ_LOG(1,(THIS_FILE, "Unable to retrieve local host IP"));
619 goto on_error;
620 }
621 pj_sockaddr_print(&hostaddr, hostip, sizeof(hostip), 2);
622
623 pj_ansi_sprintf(temp, "<sip:sipecho@%s:%d>",
624 hostip, sip_port);
625 local_uri = pj_str(temp);
626
627 call = &app.call[0];
628
629 status = pjsip_dlg_create_uac( pjsip_ua_instance(),
630 &local_uri, /* local URI */
631 &local_uri, /* local Contact */
632 &dst_uri, /* remote URI */
633 &dst_uri, /* remote target */
634 &dlg); /* dialog */
635 if (status != PJ_SUCCESS) {
636 app_perror(THIS_FILE, "Unable to create UAC dialog", status);
637 return 1;
638 }
639
640 status = pjsip_inv_create_uac( dlg, NULL, 0, &call->inv);
641 if (status != PJ_SUCCESS) goto on_error;
642
643 call->inv->mod_data[mod_sipecho.id] = call;
644
645 status = pjsip_inv_invite(call->inv, &tdata);
646 if (status != PJ_SUCCESS) goto on_error;
647
648 status = pjsip_inv_send_msg(call->inv, tdata);
649 if (status != PJ_SUCCESS) goto on_error;
650
651 puts("Press ENTER to quit...");
652 } else {
653 puts("Ready for incoming calls. Press ENTER to quit...");
654 }
655
656 for (;;) {
657 char s[10];
658
659 printf("\nMenu:\n"
660 " h Hangup all calls\n"
661 " l %s message logging\n"
662 " q Quit\n",
663 (app.enable_msg_logging? "Disable" : "Enable"));
664
665 if (fgets(s, sizeof(s), stdin) == NULL)
666 continue;
667
668 if (s[0]=='q')
669 break;
670 switch (s[0]) {
671 case 'l':
672 app.enable_msg_logging = !app.enable_msg_logging;
673 break;
674 case 'h':
675 hangup_all();
676 break;
677 }
678 }
679
680 destroy_stack();
681
682 puts("Bye bye..");
683 return 0;
684
685on_error:
686 puts("An error has occurred. run a debugger..");
687 return 1;
688}
689