1 /*
2 *
3 * Copyright (c) 2003 The Regents of the University of California. All
4 * rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * - Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 *
13 * - Neither the name of the University nor the names of its
14 * contributors may be used to endorse or promote products derived
15 * from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS''
18 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
19 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
20 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
21 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
25 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 *
29 */
30
31
32 #include "common.h"
33 #include "gls_i.h"
34 #include "gls_nblist.h"
35 #include "hash.h"
36 #include "dijkstra.h"
37
38 extern node_id_t my_node_id;
39 extern loc_t my_loc;
40
41 /* what's the upper bound on how often we wake up and send updates
42 * (it's specified in milliseconds) */
43 #define INTER_UPDATE_INTERVAL_MSEC 500
44
45 #define MY_LOG_LEVEL LOG_DEBUG(4)
46 //#define MY_LOG_LEVEL LOG_NOTICE
47
48 /******************* Utility Functions **********************/
49
50 /* static void */
51 /* print_nb_list(int count, neighbor_t *nb_list, buf_t *b) */
52 /* { */
53 /* int i; */
54
55 /* assert(count);assert(nb_list);assert(b); */
56
57 /* bufprintf(b,"NeighborList(%d):",nb_list[i].node_id); */
58
59 /* for (i = 0; i < count; i++) { */
60
61 /* Process only ACTIVE neighbors */
62 /* if (nb_list[i].state == ACTIVE) { */
63 /* bufprintf(b,":%d ",nb_list[i].node_id); */
64 /* } */
65 /* } */
66 /* } */
67
68
69 static GSList *
70 get_active_nbr_glist(int count, neighbor_t *nb_list)
71 {
72 int i;
73 GSList *neighborlist = NULL;
74 gls_neighbor_t *n;
75
76 assert(count);
77 assert(nb_list);
78
79 for (i = 0; i < count; i++) {
80
81 /* Process only ACTIVE neighbors */
82 if (nb_list[i].state == ACTIVE) {
83
84 /* allocate memory for n */
85 n = g_malloc0(sizeof(struct neighbor));
86
87 elog(MY_LOG_LEVEL, "ACTIVE nbr: %d",nb_list[i].node_id);
88
89 /* make a copy of the neighbor element */
90 n->id = nb_list[i].node_id;
91 /* and set its default weight */
92 n->weight = 1;
93 /* and its interface */
94 /* n->if_id = nb_list[i].if_id; */
95
96 /* Prepend new element into neighborlist */
97 neighborlist = g_slist_prepend(neighborlist, (gpointer)n);
98 }
99 }
100
101 return neighborlist;
102 }
103
104
105 static gboolean
106 remove_neighbor(gpointer key, gpointer value, gpointer user_data)
107 {
108 assert(value);
109 /* destroy function is going to be called if this function is called by
110 * g_hash_table_foreach_remove, and it won't be called if
111 * g_hash_table_foreach_steal is called */
112 return TRUE;
113 }
114
115 /******************* End Utility Functions **********************/
116
117
118 /* Convert nb_list to struct update.
119 * Does not allocate anything and returns NULL on errors
120 */
121 static struct update *
122 get_nbr_update(node_id_t id, loc_t loc, GSList *nb_gslist)
123 {
124 struct update *u = NULL;
125 buf_t *b = buf_new();
126
127 /* allocate update structure. 'g_malloc' aborts prog. on failure */
128 u = g_malloc0(sizeof(struct update));
129
130 /* Assign id of node whose nb_list is being processed */
131 u->id = id;
132
133 /* Update location info for node "id" */
134 u->geo_update_present = TRUE;
135 u->geo.x = loc_trunc(loc.x);
136 u->geo.y = loc_trunc(loc.y);
137
138 /* no udp update */
139 u->udp_update_present = FALSE;
140
141 /* initialize a list of neighbors to NULL */
142 u->neighbors = nb_gslist;
143
144 elog(MY_LOG_LEVEL, "id:%d loc.x:%f loc.y:%f ",
145 id, loc.x, loc.y);
146
147 /* print_nb_list (count, nb_list, b); */
148 /* elog (MY_LOG_LEVEL, "\n%s",b->buf); */
149
150 buf_free(b);
151
152
153 return u;
154 }
155
156 static void
157 apply_update(struct update *u, struct routedev_info *tables)
158 {
159 gls_neighbor_t *n;
160 GSList *cur;
161 struct nodeconf *nodecfg;
162 gboolean insert_udp = TRUE;
163 gboolean insert_geo = TRUE;
164
165 assert(u);
166 assert(tables);
167 assert(tables->nodeid);
168 assert(tables->geo);
169 assert(tables->udp);
170
171 /* check if the node is present in nodeid table. If it isn't, we add it to
172 * all tables, since having a node present in nodeid table ensures it is
173 * present in other tables as well */
174 if ((nodecfg = g_hash_table_lookup(tables->nodeid, & u->id))) /* ... yep */
175 {
176
177 elog(MY_LOG_LEVEL, "Node %d PRESENT in tables->nodeid",u->id);
178
179 /* check to make sure udp information did not change. If it did, remove
180 * the old udp table entry, and enter new one */
181 if ( (u->udp_update_present)
182 && ( (nodecfg->udp.ip.s_addr != u->udp.ip.s_addr)
183 || (nodecfg->udp.port != u->udp.port)))
184 {
185 /* if present, pointer to node will be removed from the udp table */
186 g_hash_table_remove(tables->udp, & nodecfg->udp);
187 }
188 else
189 {
190 /* we don't remove nodecfg->udp, so no need to re-insert it */
191 insert_udp = FALSE;
192 }
193
194 /* check to make sure geo information did not change. If it did, remove
195 * the old geo entry and, and enter new one */
196 if ( (u->geo_update_present)
197 && ( (nodecfg->geo.x != u->geo.x)
198 || (nodecfg->geo.y != u->geo.y)))
199 {
200 /* if present, pointer to node will be removed from geo table */
201 g_hash_table_remove(tables->geo, & nodecfg->geo);
202 }
203 else
204 {
205 /* we don't remove nodecfg->geo, so no need to re-insert it */
206 insert_geo = FALSE;
207 }
208
209 /* it is easier to remove/reenter neighbors, then to try to figure out
210 * if they have changed. Also, it would probably be a little more
211 * efficient to destroy/recreate table, as opposed to not destroying a
212 * table, but removing all elements. However, then we'd have an extra
213 * place where we create this neighborlist table, and that would not be
214 * good - more places to keep track of if something changes */
215 g_hash_table_foreach_remove(nodecfg->neighbor_list, remove_neighbor ,
216 NULL);
217 }
218 else /* node for which we received an update is not present in table */
219 {
220
221 elog(MY_LOG_LEVEL, "Node%d INSERTED in tables->nodeid",u->id);
222
223 /* allocate a node */
224 nodecfg = g_new0(struct nodeconf, 1);
225 /* initialize our structure */
226 init_nodeconf(nodecfg);
227
228 /* set structure fields, which are different from the defaults */
229 nodecfg->id = u->id;
230
231 /* add the node to nodeid table */
232 g_hash_table_insert(tables->nodeid, &(nodecfg->id), nodecfg);
233 }
234
235 /* store updated udp/geo info, since either the key has changed, or this is
236 * a new addition (if updated information was present, of course) */
237 if ((u->udp_update_present) && (insert_udp))
238 {
239 /* update udp information */
240 nodecfg->udp = u->udp;
241 /* make sure we can find this node by its udp info */
242 g_hash_table_insert(tables->udp, & nodecfg->udp, nodecfg);
243 }
244
245 if ((u->geo_update_present) && (insert_geo))
246 {
247 elog (MY_LOG_LEVEL, "Updating GEO with address NODEID:%d LOC:%2.2f,%2.2f",
248 u->id, 0.0, 0.0);//u->geo.x, u->geo.y);
249 /* update geo information */
250 nodecfg->geo = u->geo;
251 /* make sure we can find this node by it's geo information */
252 g_hash_table_insert(tables->geo, & nodecfg->geo, nodecfg);
253 }
254
255 /* either re-add, or newly-add neighbors (depending on whether the node was
256 * found in the table or not) */
257 for (cur = u->neighbors; cur; cur = g_slist_next(cur))
258 {
259 assert(cur->data);
260
261 n = g_new0(gls_neighbor_t, 1);
262 n->id = ((gls_neighbor_t *)cur->data)->id;
263 // n->if_id = ((gls_neighbor_t *)cur->data)->if_id;
264 n->weight = ((gls_neighbor_t *)cur->data)->weight;
265 g_hash_table_insert(nodecfg->neighbor_list, &(n->id), n);
266 }
267 }
268
269 static int
270 recompute_timer(void *data, int interval, g_event_t *event)
271 {
272 gpointer tofree;
273 struct update *u;
274 gls_state_t *args = (gls_state_t *)data;
275
276 assert(event);
277 assert(data);
278 assert(args);
279
280 elog (MY_LOG_LEVEL, "Enter");
281
282 /* the timer would only go off for us to process updates, so there must be
283 * some... */
284 assert(args->updates);
285 /* tables must've been created by now (they could be empty, but they must
286 * exist) */
287 assert(args->info);
288 /* note, that since routing table is dynamically
289 * allocated/deallocated/reallocated when we run dijkstra, it may not exist
290 * until compute_routing_table is run */
291 assert(args->info->nodeid);
292 assert(args->info->geo);
293 assert(args->info->udp);
294
295 /* loop through updates */
296 while(! g_queue_is_empty(args->updates))
297 {
298 /* get next update from the head. we checked for g_queue_is_empty, so
299 * should never return NULL */
300 u = g_queue_pop_head(args->updates);
301 assert(u);
302
303 /* apply update, but don't recompute a routing table just yet */
304 apply_update(u, args->info);
305
306 /*
307 * done with this update - destroy it
308 */
309
310 /* first, destroy the neighbors (we could've instead opted for not
311 * removing neighbors here, and simply stored them in the neighborlist
312 * during apply_update (as opposed to making copies there). This,
313 * however, seems a little cleaner. If performance is an issue, we could
314 * change that later */
315 while (u->neighbors)
316 {
317 /* once unlinked, we'll free this (that's the neighbor struct). we
318 * don't do it now, since g_slist_remove will be referencing 'data'
319 * pointer, and we don't want it to reference freed memory */
320 tofree = u->neighbors->data;
321 /* g_slist_remove removes the first element matching data. Since
322 * we're always @head of the list, we'll remove the element at the
323 * head and return a new list start) */
324 u->neighbors = g_slist_remove(u->neighbors, u->neighbors->data);
325 /* now free the data */
326 g_free(tofree);
327 }
328
329 /* now, destroy the update structure */
330 g_free(u);
331 }
332
333 /* destroy the timer (we don't need to keep firing if no more updates are
334 * coming in) */
335 if (0 > (g_event_destroy(args->recompute_timer)))
336 {
337 printf("handle_updates: Error destroying a timer\n");
338 return EVENT_ERROR(-1);
339 }
340
341 args->recompute_timer = NULL;
342
343 /* reset the number of times we were overidden */
344 args->num_timer_resets = 0;
345
346
347 elog(MY_LOG_LEVEL, "Printing global table for %d",my_node_id);
348
349 /* print the entire neighbor table for debugging */
350 print_global_table((args->info)->nodeid);
351
352
353 /* Finally, recompute the routing table itself */
354 compute_routing_table(args->info, args->nodecfg);
355
356 elog(MY_LOG_LEVEL, "Printing ROUTING TABLE");
357
358 print_routing_table((args->info)->rtable);
359
360
361 #if 0 /* run debug querries. works with route.conf.3 file */
362
363 {
364 printf("----------------------------------------\n");
365 printf("DEBUG QUERRIES - lookup nexthop info\n");
366 printf("----------------------------------------\n");
367 run_debug_queries("0.0.0.0", 0, 0, 0, 0, args->info);
368 printf("----------------------------------------\n");
369 run_debug_queries("1.1.1.1", 1, 1, 1, 1, args->info);
370 printf("----------------------------------------\n");
371 run_debug_queries("2.2.2.2", 2, 2, 2, 2, args->info);
372 printf("----------------------------------------\n");
373 run_debug_queries("3.3.3.3", 3, 3, 3, 3, args->info);
374 printf("----------------------------------------\n");
375 run_debug_queries("4.4.4.4", 4, 4, 4, 4, args->info);
376 printf("----------------------------------------\n");
377 }
378 #endif
379
380 return EVENT_RENEW;
381 }
382
383
384 /* neighbor_t *nb_list, int count, void *data */
385 static int
386 new_neighbor_update(node_id_t id, loc_t loc,
387 GSList *nb_gslist, gls_state_t *args)
388 {
389 gboolean start_recompute_timer = FALSE;
390 struct update *u;
391
392 if ((u = get_nbr_update(id, loc, nb_gslist)))
393 {
394 /* append an update to the queue */
395 g_queue_push_tail(args->updates, (gpointer)u);
396
397 if (args->recompute_timer) /* recompute timer is active */
398 {
399 /* do we want to reset it? We do if we haven't reset it more then
400 * allowed number of times. Otherwise, we just wait for the timer to
401 * go off and process the updates without interfering */
402 if (args->num_timer_resets < args->max_timer_resets)
403 {
404 start_recompute_timer = TRUE;
405 args->num_timer_resets ++;
406
407 /* destroy the timer that's there now. That's because we dont
408 * have a way to stop/start timers */
409 if (0 > (g_event_destroy(args->recompute_timer)))
410 {
411 printf("handle_updates: Error destroying a timer\n");
412 return EVENT_ERROR(-1);
413 }
414
415 args->recompute_timer = NULL;
416 }
417 }
418 else /* recompute timer is not active */
419 {
420 /* we then start it. 'num_timer_resets' is not incremented, since
421 * we assume that the first time the timer is started, it does not
422 * count as reset */
423 start_recompute_timer = TRUE;
424 }
425
426 /* if necessary, start the timer which will then go off and process the
427 * updates */
428 if (start_recompute_timer)
429 {
430 /* timer options */
431 g_event_opts_t timer_opts;
432
433 memset(&timer_opts, 0, sizeof timer_opts);
434 timer_opts.event_name = "recompute timer";
435
436 if (0 > g_timer_add(args->recompute_timer_sleeptime,
437 recompute_timer,
438 args,
439 &timer_opts,
440 &args->recompute_timer))
441 {
442 printf("handle_updates: g_timer_add failed\n");
443 return EVENT_ERROR(-1);
444 }
445 }
446 }
447 else /* unsuccessful at receiving an update */
448 {
449 printf("handle_updates: Error receiving an update\n");
450 return EVENT_RENEW;
451 }
452
453 return EVENT_RENEW;
454 }
455
456 /* Returns size of the allocated flood packet */
457 /* static void marshal_flood_pkt (char *pkt, int pkt_size, node_id_t id, */
458 /* loc_t loc, int count, */
459 /* neighbor_t *nb_list) */
460 /* { */
461 /* flood_pkt_t *flood_pkt = (flood_pkt_t *) pkt; */
462
463 /* /\* Allocate flood packet *\/ */
464 /* flood_pkt->id = id; */
465 /* flood_pkt->loc = loc; */
466 /* flood_pkt->count = count; */
467
468 /* /\* Copy in the constant sized fields *\/ */
469 /* memcpy(pkt, flood_pkt, sizeof(flood_pkt_t)); */
470
471 /* /\* and the variable sized nb_list *\/ */
472 /* memcpy(flood_pkt->nb_list, */
473 /* (void *)nb_list, count * sizeof(neighbor_t)); */
474 /* } */
475
476 static void marshall_gslist (gpointer data, gpointer user_data)
477 {
478 buf_t *buf = (buf_t *) user_data;
479 gls_neighbor_t *n = (gls_neighbor_t *)data;
480
481 elog(MY_LOG_LEVEL, "Copying Nbr:%d Weight:%d", (int)n->id, (int)n->weight);
482 bufcpy(buf, n, sizeof(gls_neighbor_t));
483 }
484
485 static int flood_send_nb_list_update(node_id_t id, loc_t loc,
486 GSList *nb_gslist, gls_state_t *gls_state)
487 {
488
489 buf_t *pkt_out = buf_new();
490
491 /* Create our *outgoing* link header */
492 link_pkt_t link_hdr = {
493 dst: {
494 id: LINK_BROADCAST,
495 },
496 type: PKT_TYPE_FLOOD_GLS,
497 };
498
499 flood_pkt_t flood_hdr;
500
501 flood_hdr.id = id;
502 flood_hdr.loc = loc;
503 flood_hdr.count = g_slist_length(nb_gslist);
504
505 if (flood_hdr.count > 0) {
506
507 bufcpy(pkt_out, &link_hdr, sizeof(link_hdr));
508 bufcpy(pkt_out, &flood_hdr, sizeof(flood_hdr));
509
510 /* copy each element of nb_gslist into the packet */
511 g_slist_foreach(nb_gslist, (GFunc) marshall_gslist, (gpointer)pkt_out);
512
513 elog(MY_LOG_LEVEL, "count:%d",flood_hdr.count);
514
515 /* now launch the packet! */
516 if (link_send(gls_state->flood_link, (link_pkt_t *)pkt_out->buf, pkt_out->len) < 0)
517 elog(LOG_ERR, "can't send on %s: %m", link_name(link_opts(gls_state->flood_link)));
518 else
519 elog(MY_LOG_LEVEL, "broadcast nb_list");
520
521 }
522
523 free(pkt_out);
524 return 1;
525 }
526
527 static int
528 gls_flood_timer(void *data, int interval, g_event_t *event)
529 {
530 gls_state_t *gs = (gls_state_t *)data;
531 GSList *nb_gslist;
532
533 /* set the number of holdoffs to 0 */
534 gs->num_holdoffs = 0;
535
536 /* Convert the nb_list into a GSList*/
537 nb_gslist = get_active_nbr_glist(gs->nb_list_count,gs->nb_list);
538
539 elog(LOG_DEBUG_0, "Flooding nb_list");
540
541 /* send the neighbor list to the entire network */
542 flood_send_nb_list_update(my_node_id, my_loc,
543 nb_gslist, gs);
544
545 return TIMER_DONE;
546 }
547
548
549 /* Update routing tables when new nb_list is received */
550 static int
551 local_nb_list_update(neighbor_t *nb_list, int count, void *data) {
552 gls_state_t *gls_state = (gls_state_t *) data;
553 GSList *nb_gslist;
554
555 elog(LOG_DEBUG_0, "got local neighbor list");
556
557 /* free the previously stored nb_list and store the current one */
558 if (gls_state->nb_list != NULL) free (gls_state->nb_list);
559 gls_state->nb_list = nb_list;
560 gls_state->nb_list_count = count;
561
562 /* Convert the nb_list into a GSList*/
563 nb_gslist = get_active_nbr_glist(count,nb_list);
564
565 /* schedule our tables for updation with the new neighbor list */
566 new_neighbor_update(my_node_id, my_loc, nb_gslist,
567 (gls_state_t *) data);
568
569 /* Set timeout for apps to flood */
570 /* If number of holdoffs > predefined value, then dont reschedule */
571 if (g_timer_isset(gls_state->holdoff_timer_event) &&
572 gls_state->num_holdoffs<MAX_HOLDOFFS) {
573 /* increment the number of holdoffs */
574 gls_state->num_holdoffs++;
575 /* reschedule the timer */
576 if (g_timer_resched(gls_state->holdoff_timer_event, FLOOD_TIMEOUT) < 0) {
577 elog(LOG_CRIT,"gls_flood_timer: g_timer_resched failed");
578 exit(1);
579 }
580 } else {
581 gls_state->num_holdoffs = 0;
582 if (g_timer_add(FLOOD_TIMEOUT, gls_flood_timer,
583 gls_state, NULL, &(gls_state->holdoff_timer_event)) < 0)
584 {
585 elog(LOG_CRIT,"gls_flood_timer: g_timer_add failed");
586 exit(1);
587 }
588 }
589
590 return EVENT_RENEW;
591 }
592
593 static
594 int flood_receive_nb_list_update(link_pkt_t *link_pkt,
595 ssize_t data_len, link_context_t *link)
596 {
597 GSList *nb_gslist = NULL;
598 gls_neighbor_t *n;
599
600 /* Extract gls_state */
601 gls_state_t *gls_state = (gls_state_t *) link_opts(link)->data;
602
603 /* Extract flood_pkt from link_pkt */
604 flood_pkt_t *flood_pkt = (flood_pkt_t *) link_pkt->data;
605
606 /* Extract gls_nb_list from flood data */
607 gls_neighbor_t *gls_nb_list = (gls_neighbor_t *) flood_pkt->gls_nb_list;
608
609 int i;
610
611 elog(LOG_DEBUG_0, "Received Flooded Neighbor List from %d: Count: %d",
612 flood_pkt->id, flood_pkt->count);
613
614 /* generate GSList from gls_neighbor_t */
615 for (i=0; i<flood_pkt->count; i++) {
616 /* allocate memory for n */
617 n = g_malloc0(sizeof(gls_neighbor_t));
618
619 *n = gls_nb_list[i];
620
621 elog(MY_LOG_LEVEL, "Nbr[%d] = %d Weight=%d",
622 flood_pkt->id,(int)gls_nb_list[i].id,(int)gls_nb_list[i].weight);
623
624 /* Prepend new element into neighborlist */
625 nb_gslist = g_slist_prepend(nb_gslist, (gpointer)n);
626 }
627
628 elog(MY_LOG_LEVEL, "Received nbr_list from %d", flood_pkt->id);
629
630 new_neighbor_update(flood_pkt->id, flood_pkt->loc,
631 nb_gslist, gls_state);
632
633 free (link_pkt);
634
635 return 1;
636 }
637
638 /*
639 * Come up, process configuration file to determine node topology, and create a
640 * routing table. Idle away, waiting for clients' "next hop" requests, and
641 * respond to them. Operates as a 'ROUTE_ROUTEDEV_NAME' device (set in the
642 * gls_i.h)
643 */
644 void
645 gls_nblist_open(gls_state_t *gls_state)
646 {
647
648 neighbor_opts_t n_opts = {
649 link_opts: {
650 link_index: LINK_INDEX_AUTO
651 },
652 new_list: local_nb_list_update,
653 data: (void *) gls_state /* routing state */
654 };
655
656 /* nbr flood config */
657 link_opts_t flood_link_opts = {
658 link_index: LINK_INDEX_AUTO, /* use any available link */
659 pkt_type: PKT_TYPE_FLOOD_GLS, /* only give us type */
660 dev_type: LINK_DEV_FLOOD, /* flood device */
661 q_opts: {
662 outq_len: 10000,
663 inq_len: 10000,
664 },
665 receive: flood_receive_nb_list_update, /* call this func when packets arrive */
666 data: (void *) gls_state /* routing state */
667 };
668
669 /* Open a link to the neighborlist service
670 * We're only listening on this daemon, so dont save
671 * link state.
672 */
673 if (g_neighbors(&n_opts, NULL) < 0) {
674 elog(LOG_CRIT, "couldn't open neighborlist %s: %m",
675 link_name(&n_opts.link_opts));
676 exit(1);
677 }
678
679 /*
680 * Open the link-layer device for receiving nb_lists that were flooded
681 * from other nodes.
682 * Save link state for later use.
683 */
684 if (link_open(&flood_link_opts, &gls_state->flood_link) < 0) {
685 elog(LOG_CRIT, "can't open %s: %m", link_name(&flood_link_opts));
686 exit(1);
687 }
688
689 elog(MY_LOG_LEVEL, "gls_nblist running");
690 }
691
692
This page was automatically generated by the
LXR engine.
Visit the LXR main site for more
information.