slave.c 22 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
/* Icecast
 *
 * This program is distributed under the GNU General Public License, version 2.
 * A copy of this license is included with this source.
 *
 * Copyright 2000-2004, Jack Moffitt <jack@xiph.org, 
 *                      Michael Smith <msmith@xiph.org>,
 *                      oddsock <oddsock@xiph.org>,
 *                      Karl Heyes <karl@xiph.org>
 *                      and others (see AUTHORS for details).
 */

13
/* -*- c-basic-offset: 4; indent-tabs-mode: nil; -*- */
14 15 16 17 18 19 20
/* slave.c
 * by Ciaran Anscomb <ciaran.anscomb@6809.org.uk>
 *
 * Periodically requests a list of streams from a master server
 * and creates source threads for any it doesn't already have.
 * */

21 22 23 24
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

#ifndef _WIN32
#include <sys/socket.h>
#include <netinet/in.h>
#else
#include <winsock2.h>
#define snprintf _snprintf
#define strcasecmp stricmp
#define strncasecmp strnicmp
#endif

40
#include "compat.h"
41

Karl Heyes's avatar
Karl Heyes committed
42 43 44 45
#include "thread/thread.h"
#include "avl/avl.h"
#include "net/sock.h"
#include "httpp/httpp.h"
46

47
#include "cfgfile.h"
48 49 50 51 52 53 54 55
#include "global.h"
#include "util.h"
#include "connection.h"
#include "refbuf.h"
#include "client.h"
#include "stats.h"
#include "logging.h"
#include "source.h"
Michael Smith's avatar
Michael Smith committed
56
#include "format.h"
57
#include "event.h"
58 59 60 61

#define CATMODULE "slave"

static void *_slave_thread(void *arg);
62
static thread_type *_slave_thread_id;
63
static int slave_running = 0;
64
static volatile int update_settings = 0;
65
static volatile int update_all_mounts = 0;
66
static volatile unsigned int max_interval = 0;
67

68
relay_server *relay_free (relay_server *relay)
69
{
70 71 72 73 74 75 76
    relay_server *next = relay->next;
    DEBUG1("freeing relay %s", relay->localmount);
    if (relay->source)
       source_free_source (relay->source);
    xmlFree (relay->server);
    xmlFree (relay->mount);
    xmlFree (relay->localmount);
77 78 79 80
    if (relay->username)
        xmlFree (relay->username);
    if (relay->password)
        xmlFree (relay->password);
81
    free (relay);
82
    return next;
83 84
}

85

86 87 88
relay_server *relay_copy (relay_server *r)
{
    relay_server *copy = calloc (1, sizeof (relay_server));
Michael Smith's avatar
Michael Smith committed
89

90
    if (copy)
Michael Smith's avatar
Michael Smith committed
91
    {
92 93 94
        copy->server = (char *)xmlCharStrdup (r->server);
        copy->mount = (char *)xmlCharStrdup (r->mount);
        copy->localmount = (char *)xmlCharStrdup (r->localmount);
95
        if (r->username)
96
            copy->username = (char *)xmlCharStrdup (r->username);
97
        if (r->password)
98
            copy->password = (char *)xmlCharStrdup (r->password);
99 100
        copy->port = r->port;
        copy->mp3metadata = r->mp3metadata;
101
        copy->on_demand = r->on_demand;
Michael Smith's avatar
Michael Smith committed
102
    }
103 104
    return copy;
}
105

106

107
/* force a recheck of the relays. This will recheck the master server if
108
 * this is a slave and rebuild all mountpoints in the stats tree
109
 */
110
void slave_update_all_mounts (void)
111
{
112
    max_interval = 0;
113
    update_all_mounts = 1;
114
    update_settings = 1;
115 116
}

117 118 119

/* Request slave thread to check the relay list for changes and to
 * update the stats for the current streams.
120
 */
121
void slave_rebuild_mounts (void)
122
{
123
    update_settings = 1;
124 125
}

126 127

void slave_initialize(void)
Michael Smith's avatar
Michael Smith committed
128
{
129 130
    if (slave_running)
        return;
Michael Smith's avatar
Michael Smith committed
131

132
    slave_running = 1;
133
    max_interval = 0;
134 135
    _slave_thread_id = thread_create("Slave Thread", _slave_thread, NULL, THREAD_ATTACHED);
}
136

Michael Smith's avatar
Michael Smith committed
137

138 139 140
void slave_shutdown(void)
{
    if (!slave_running)
141
        return;
142
    slave_running = 0;
143
    DEBUG0 ("waiting for slave thread");
144 145 146 147
    thread_join (_slave_thread_id);
}


148 149
/* Actually open the connection and do some http parsing, handle any 302
 * responses within here.
150
 */
151
static client_t *open_relay_connection (relay_server *relay)
152
{
153
    int redirects = 0;
154 155
    char *server_id = NULL;
    ice_config_t *config;
156 157
    http_parser_t *parser = NULL;
    connection_t *con=NULL;
158 159 160 161
    char *server = strdup (relay->server);
    char *mount = strdup (relay->mount);
    int port = relay->port;
    char *auth_header;
162 163
    char header[4096];

164 165 166 167
    config = config_get_config ();
    server_id = strdup (config->server_id);
    config_release_config ();

168 169
    /* build any authentication header before connecting */
    if (relay->username && relay->password)
170
    {
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
        char *esc_authorisation;
        unsigned len = strlen(relay->username) + strlen(relay->password) + 2;

        auth_header = malloc (len);
        snprintf (auth_header, len, "%s:%s", relay->username, relay->password);
        esc_authorisation = util_base64_encode(auth_header);
        free(auth_header);
        len = strlen (esc_authorisation) + 24;
        auth_header = malloc (len);
        snprintf (auth_header, len,
                "Authorization: Basic %s\r\n", esc_authorisation);
        free(esc_authorisation);
    }
    else
        auth_header = strdup ("");
186

187 188 189 190 191 192
    while (redirects < 10)
    {
        sock_t streamsock;

        INFO2 ("connecting to %s:%d", server, port);

193
        streamsock = sock_connect_wto_bind (server, port, relay->bind, 10);
194 195
        if (streamsock == SOCK_ERROR)
        {
196
            WARN2 ("Failed to connect to %s:%d", server, port);
197
            break;
Michael Smith's avatar
Michael Smith committed
198
        }
199
        con = connection_create (streamsock, -1, strdup (server));
200

201 202 203 204 205 206
        /* At this point we may not know if we are relaying an mp3 or vorbis
         * stream, but only send the icy-metadata header if the relay details
         * state so (the typical case).  It's harmless in the vorbis case. If
         * we don't send in this header then relay will not have mp3 metadata.
         */
        sock_write(streamsock, "GET %s HTTP/1.0\r\n"
207
                "User-Agent: %s\r\n"
Karl Heyes's avatar
Karl Heyes committed
208
                "Host: %s\r\n"
209
                "%s"
210
                "%s"
211
                "\r\n",
212
                mount,
213
                server_id,
Karl Heyes's avatar
Karl Heyes committed
214
                server,
215 216
                relay->mp3metadata?"Icy-MetaData: 1\r\n":"",
                auth_header);
217
        memset (header, 0, sizeof(header));
218
        if (util_read_header (con->sock, header, 4096, READ_ENTIRE_HEADER) == 0)
219
        {
220
            ERROR4 ("Header read failed for %s (%s:%d%s)", relay->localmount, server, port, mount);
221 222 223 224 225 226
            break;
        }
        parser = httpp_create_parser();
        httpp_initialize (parser, NULL);
        if (! httpp_parse_response (parser, header, strlen(header), relay->localmount))
        {
227 228
            ERROR4("Error parsing relay request for %s (%s:%d%s)", relay->localmount,
                    server, port, mount);
229 230
            break;
        }
231
        if (strcmp (httpp_getvar (parser, HTTPP_VAR_ERROR_CODE), "302") == 0)
232
        {
233 234 235
            /* better retry the connection again but with different details */
            const char *uri, *mountpoint;
            int len;
236

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
            uri = httpp_getvar (parser, "location");
            INFO1 ("redirect received %s", uri);
            if (strncmp (uri, "http://", 7) != 0)
                break;
            uri += 7;
            mountpoint = strchr (uri, '/');
            free (mount);
            if (mountpoint)
                mount = strdup (mountpoint);
            else
                mount = strdup ("/");

            len = strcspn (uri, ":/");
            port = 80;
            if (uri [len] == ':')
                port = atoi (uri+len+1);
            free (server);
            server = calloc (1, len+1);
            strncpy (server, uri, len);
            connection_close (con);
            httpp_destroy (parser);
258 259 260
            con = NULL;
            parser = NULL;
        }
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
        else
        {
            client_t *client = NULL;

            if (httpp_getvar (parser, HTTPP_VAR_ERROR_MESSAGE))
            {
                ERROR2("Error from relay request: %s (%s)", relay->localmount,
                        httpp_getvar(parser, HTTPP_VAR_ERROR_MESSAGE));
                break;
            }
            global_lock ();
            if (client_create (&client, con, parser) < 0)
            {
                global_unlock ();
                /* make sure only the client_destory frees these */
                con = NULL;
                parser = NULL;
                client_destroy (client);
                break;
            }
            global_unlock ();
282
            sock_set_blocking (streamsock, 0);
283 284 285
            client_set_queue (client, NULL);
            free (server);
            free (mount);
286
            free (server_id);
287 288 289 290 291 292 293 294 295
            free (auth_header);

            return client;
        }
        redirects++;
    }
    /* failed, better clean up */
    free (server);
    free (mount);
296
    free (server_id);
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
    free (auth_header);
    if (con)
        connection_close (con);
    if (parser)
        httpp_destroy (parser);
    return NULL;
}


/* This does the actual connection for a relay. A thread is
 * started off if a connection can be acquired
 */
static void *start_relay_stream (void *arg)
{
    relay_server *relay = arg;
    source_t *src = relay->source;
    client_t *client;

    INFO1("Starting relayed source at mountpoint \"%s\"", relay->localmount);
    do
    {
        client = open_relay_connection (relay);

        if (client == NULL)
            continue;

        src->client = client;
        src->parser = client->parser;
        src->con = client->con;
326 327

        if (connection_complete_source (src, 0) < 0)
328
        {
329 330 331 332
            INFO0("Failed to complete source initialisation");
            client_destroy (client);
            src->client = NULL;
            continue;
333
        }
334
        stats_event_inc(NULL, "source_relay_connections");
335
        stats_event (relay->localmount, "source_ip", client->con->ip);
336

337 338
        source_main (relay->source);

339 340 341 342 343
        if (relay->on_demand == 0)
        {
            /* only keep refreshing YP entries for inactive on-demand relays */
            yp_remove (relay->localmount);
            relay->source->yp_public = -1;
344
            relay->start = time(NULL) + 10; /* prevent busy looping if failing */
345
            slave_update_all_mounts();
346 347
        }

348
        /* we've finished, now get cleaned up */
349
        relay->cleanup = 1;
350
        slave_rebuild_mounts();
351 352

        return NULL;
353
    } while (0);  /* TODO allow looping through multiple servers */
354

355 356 357 358
    if (relay->source->fallback_mount)
    {
        source_t *fallback_source;

359
        DEBUG1 ("failed relay, fallback to %s", relay->source->fallback_mount);
360 361 362 363 364 365 366 367 368
        avl_tree_rlock(global.source_tree);
        fallback_source = source_find_mount (relay->source->fallback_mount);

        if (fallback_source != NULL)
            source_move_clients (relay->source, fallback_source);

        avl_tree_unlock (global.source_tree);
    }

369
    source_clear_source (relay->source);
370

371
    /* cleanup relay, but prevent this relay from starting up again too soon */
Karl Heyes's avatar
Karl Heyes committed
372
    relay->source->on_demand = 0;
373
    relay->start = time(NULL) + max_interval;
374 375 376
    relay->cleanup = 1;

    return NULL;
377 378 379 380 381 382 383 384
}


/* wrapper for starting the provided relay stream */
static void check_relay_stream (relay_server *relay)
{
    if (relay->source == NULL)
    {
385 386 387 388 389 390
        if (relay->localmount[0] != '/')
        {
            WARN1 ("relay mountpoint \"%s\" does not start with /, skipping",
                    relay->localmount);
            return;
        }
391 392
        /* new relay, reserve the name */
        relay->source = source_reserve (relay->localmount);
393
        if (relay->source)
394
        {
395
            DEBUG1("Adding relay source at mountpoint \"%s\"", relay->localmount);
396
            if (relay->on_demand)
Karl Heyes's avatar
Karl Heyes committed
397 398 399 400 401 402 403
            {
                ice_config_t *config = config_get_config ();
                mount_proxy *mountinfo = config_find_mount (config, relay->localmount);
                if (mountinfo == NULL)
                    source_update_settings (config, relay->source, mountinfo);
                config_release_config ();
                stats_event (relay->localmount, "listeners", "0");
404
                slave_update_all_mounts();
Karl Heyes's avatar
Karl Heyes committed
405
            }
406
        }
407
        else
408 409 410 411 412 413 414 415
        {
            if (relay->start == 0)
            {
                WARN1 ("new relay but source \"%s\" already exists", relay->localmount);
                relay->start = 1;
            }
            return;
        }
416
    }
417
    do
418
    {
419
        source_t *source = relay->source;
420 421
        /* skip relay if active, not configured or just not time yet */
        if (relay->source == NULL || relay->running || relay->start > time(NULL))
422
            break;
423
        /* check if an inactive on-demand relay has a fallback that has listeners */
424
        if (relay->on_demand && source->on_demand_req == 0)
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
        {
            relay->source->on_demand = relay->on_demand;

            if (source->fallback_mount && source->fallback_override)
            {
                source_t *fallback;
                avl_tree_rlock (global.source_tree);
                fallback = source_find_mount (source->fallback_mount);
                if (fallback && fallback->running && fallback->listeners)
                {
                   DEBUG2 ("fallback running %d with %lu listeners", fallback->running, fallback->listeners);
                   source->on_demand_req = 1;
                }
                avl_tree_unlock (global.source_tree);
            }
            if (source->on_demand_req == 0)
                break;
        }

444
        relay->start = time(NULL) + 5;
445
        relay->running = 1;
446 447 448
        relay->thread = thread_create ("Relay Thread", start_relay_stream,
                relay, THREAD_ATTACHED);
        return;
449

450
    } while (0);
451
    /* the relay thread may of shut down itself */
452
    if (relay->cleanup)
453
    {
454 455 456 457 458 459
        if (relay->thread)
        {
            DEBUG1 ("waiting for relay thread for \"%s\"", relay->localmount);
            thread_join (relay->thread);
            relay->thread = NULL;
        }
460 461
        relay->cleanup = 0;
        relay->running = 0;
462

463
        if (relay->on_demand && relay->source)
464 465 466 467 468 469 470
        {
            ice_config_t *config = config_get_config ();
            mount_proxy *mountinfo = config_find_mount (config, relay->localmount);
            source_update_settings (config, relay->source, mountinfo);
            config_release_config ();
            stats_event (relay->localmount, "listeners", "0");
        }
Michael Smith's avatar
Michael Smith committed
471
    }
472 473
}

Michael Smith's avatar
Michael Smith committed
474

475 476 477 478 479 480 481 482 483 484 485 486 487
/* compare the 2 relays to see if there are any changes, return 1 if
 * the relay needs to be restarted, 0 otherwise
 */
static int relay_has_changed (relay_server *new, relay_server *old)
{
    do
    {
        if (strcmp (new->mount, old->mount) != 0)
            break;
        if (strcmp (new->server, old->server) != 0)
            break;
        if (new->port != old->port)
            break;
488 489
        if (new->mp3metadata != old->mp3metadata)
            break;
490 491
        if (new->on_demand != old->on_demand)
            old->on_demand = new->on_demand;
492 493 494 495 496 497
        return 0;
    } while (0);
    return 1;
}


498 499 500 501 502 503 504 505 506 507 508 509 510
/* go through updated looking for relays that are different configured. The
 * returned list contains relays that should be kept running, current contains
 * the list of relays to shutdown
 */
static relay_server *
update_relay_set (relay_server **current, relay_server *updated)
{
    relay_server *relay = updated;
    relay_server *existing_relay, **existing_p;
    relay_server *new_list = NULL;

    while (relay)
    {
511 512 513 514 515 516 517
        existing_relay = *current;
        existing_p = current;

        while (existing_relay)
        {
            /* break out if keeping relay */
            if (strcmp (relay->localmount, existing_relay->localmount) == 0)
518 519
                if (relay_has_changed (relay, existing_relay) == 0)
                    break;
520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
            existing_p = &existing_relay->next;
            existing_relay = existing_relay->next;
        }
        if (existing_relay == NULL)
        {
            /* new one, copy and insert */
            existing_relay = relay_copy (relay);
        }
        else
        {
            *existing_p = existing_relay->next;
        }
        existing_relay->next = new_list;
        new_list = existing_relay;
        relay = relay->next;
535
    }
536 537 538 539 540 541
    return new_list;
}


/* update the relay_list with entries from new_relay_list. Any new relays
 * are added to the list, and any not listed in the provided new_relay_list
542
 * are separated and returned in a separate list
543
 */
544
static relay_server *
545 546
update_relays (relay_server **relay_list, relay_server *new_relay_list)
{
547
    relay_server *active_relays, *cleanup_relays;
548

549
    active_relays = update_relay_set (relay_list, new_relay_list);
550

551 552 553 554 555 556 557 558
    cleanup_relays = *relay_list;
    /* re-assign new set */
    *relay_list = active_relays;

    return cleanup_relays;
}


559 560
static void relay_check_streams (relay_server *to_start,
        relay_server *to_free, int skip_timer)
561 562 563 564
{
    relay_server *relay;

    while (to_free)
565
    {
566
        if (to_free->source)
567
        {
568 569 570 571
            if (to_free->running)
            {
                /* relay has been removed from xml, shut down active relay */
                DEBUG1 ("source shutdown request on \"%s\"", to_free->localmount);
572
                to_free->running = 0;
573 574 575 576 577
                to_free->source->running = 0;
                thread_join (to_free->thread);
            }
            else
                stats_event (to_free->localmount, NULL, NULL);
578
        }
579 580 581 582 583 584
        to_free = relay_free (to_free);
    }

    relay = to_start;
    while (relay)
    {
585 586
        if (skip_timer)
            relay->start = 0;
587 588
        check_relay_stream (relay);
        relay = relay->next;
589
    }
Michael Smith's avatar
Michael Smith committed
590 591
}

592 593 594 595 596

static int update_from_master(ice_config_t *config)
{
    char *master = NULL, *password = NULL, *username= NULL;
    int port;
597
    sock_t mastersock;
598
    int ret = 0;
599
    char buf[256];
600 601 602
    do
    {
        char *authheader, *data;
603
        relay_server *new_relays = NULL, *cleanup_relays;
604
        int len, count = 1;
605
        int on_demand;
606

607
        username = strdup (config->master_username);
608 609
        if (config->master_password)
            password = strdup (config->master_password);
Michael Smith's avatar
Michael Smith committed
610

611 612
        if (config->master_server)
            master = strdup (config->master_server);
Michael Smith's avatar
Michael Smith committed
613

614
        port = config->master_server_port;
Michael Smith's avatar
Michael Smith committed
615

616 617
        if (password == NULL || master == NULL || port == 0)
            break;
618
        on_demand = config->on_demand;
619 620
        ret = 1;
        config_release_config();
621
        mastersock = sock_connect_wto (master, port, 10);
622

623 624 625 626 627
        if (mastersock == SOCK_ERROR)
        {
            WARN0("Relay slave failed to contact master server to fetch stream list");
            break;
        }
Michael Smith's avatar
Michael Smith committed
628

629 630 631
        len = strlen(username) + strlen(password) + 2;
        authheader = malloc(len);
        snprintf (authheader, len, "%s:%s", username, password);
632 633 634 635 636 637 638 639
        data = util_base64_encode(authheader);
        sock_write (mastersock,
                "GET /admin/streamlist.txt HTTP/1.0\r\n"
                "Authorization: Basic %s\r\n"
                "\r\n", data);
        free(authheader);
        free(data);

640 641 642 643 644 645 646 647
        if (sock_read_line(mastersock, buf, sizeof(buf)) == 0 ||
                strncmp (buf, "HTTP/1.0 200", 12) != 0)
        {
            sock_close (mastersock);
            WARN0 ("Master rejected streamlist request");
            break;
        }

648 649 650 651 652 653 654 655 656 657 658 659 660 661
        while (sock_read_line(mastersock, buf, sizeof(buf)))
        {
            if (!strlen(buf))
                break;
        }
        while (sock_read_line(mastersock, buf, sizeof(buf)))
        {
            relay_server *r;
            if (!strlen(buf))
                continue;
            DEBUG2 ("read %d from master \"%s\"", count++, buf);
            r = calloc (1, sizeof (relay_server));
            if (r)
            {
662
                r->server = (char *)xmlCharStrdup (master);
663
                r->port = port;
664 665
                r->mount = (char *)xmlCharStrdup (buf);
                r->localmount = (char *)xmlCharStrdup (buf);
666
                r->mp3metadata = 1;
667
                r->on_demand = on_demand;
668 669
                r->next = new_relays;
                new_relays = r;
670
            }
Michael Smith's avatar
Michael Smith committed
671
        }
672 673
        sock_close (mastersock);

674 675 676
        thread_mutex_lock (&(config_locks()->relay_lock));
        cleanup_relays = update_relays (&global.master_relays, new_relays);
        
677 678
        relay_check_streams (global.master_relays, cleanup_relays, 0);
        relay_check_streams (NULL, new_relays, 0);
679 680 681

        thread_mutex_unlock (&(config_locks()->relay_lock));

682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697
    } while(0);

    if (master)
        free (master);
    if (username)
        free (username);
    if (password)
        free (password);

    return ret;
}


static void *_slave_thread(void *arg)
{
    ice_config_t *config;
698
    unsigned int interval = 0;
Michael Smith's avatar
Michael Smith committed
699

700
    update_settings = 0;
701
    update_all_mounts = 0;
702

Karl Heyes's avatar
Karl Heyes committed
703 704 705
    config = config_get_config();
    stats_global (config);
    config_release_config();
706
    source_recheck_mounts (1);
707

708
    while (1)
709
    {
710 711
        relay_server *cleanup_relays = NULL;
        int skip_timer = 0;
712

713 714 715 716 717 718 719
        /* re-read xml file if requested */
        if (global . schedule_config_reread)
        {
            event_config_read (NULL);
            global . schedule_config_reread = 0;
        }

720
        thread_sleep (1000000);
721 722
        if (slave_running == 0)
            break;
723 724

        ++interval;
725

726 727 728 729 730
        /* only update relays lists when required */
        if (max_interval <= interval)
        {
            DEBUG0 ("checking master stream list");
            config = config_get_config();
Michael Smith's avatar
Michael Smith committed
731

732 733
            if (max_interval == 0)
                skip_timer = 1;
734 735
            interval = 0;
            max_interval = config->master_update_interval;
Michael Smith's avatar
Michael Smith committed
736

737 738 739
            /* the connection could take some time, so the lock can drop */
            if (update_from_master (config))
                config = config_get_config();
740

741
            thread_mutex_lock (&(config_locks()->relay_lock));
742

743
            cleanup_relays = update_relays (&global.relays, config->relay);
744

745 746 747 748
            config_release_config();
        }
        else
            thread_mutex_lock (&(config_locks()->relay_lock));
749 750 751 752 753

        relay_check_streams (global.relays, cleanup_relays, skip_timer);
        relay_check_streams (global.master_relays, NULL, skip_timer);
        thread_mutex_unlock (&(config_locks()->relay_lock));

754 755
        if (update_settings)
        {
756
            source_recheck_mounts (update_all_mounts);
757
            update_settings = 0;
758
            update_all_mounts = 0;
759
        }
760
    }
761
    INFO0 ("shutting down current relays");
762 763
    relay_check_streams (NULL, global.relays, 0);
    relay_check_streams (NULL, global.master_relays, 0);
764

765 766
    INFO0 ("Slave thread shutdown complete");

767
    return NULL;
768
}
Michael Smith's avatar
Michael Smith committed
769