/*
 * $Id: settings.c,v 1.22 2005/05/14 04:11:04 cbiere Exp $
 *
 * Copyright (c) 2001-2003, Raphael Manfredi, Richard Eckart
 *
 * gtk-gnutella configuration.
 *
 *----------------------------------------------------------------------
 * This file is part of gtk-gnutella.
 *
 *  gtk-gnutella is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  gtk-gnutella is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with gtk-gnutella; if not, write to the Free Software
 *  Foundation, Inc.:
 *      59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *----------------------------------------------------------------------
 */

#include "common.h"

#include <netdb.h>

#include "lib/eval.h"

#include "settings.h"
#include "search.h"
#include "hosts.h"
#include "upload_stats.h"
#include "sockets.h"
#include "inet.h"
#include "hcache.h"
#include "downloads.h"
#include "share.h"

#include "if/gnet_property.h"
#include "if/gnet_property_priv.h"
#include "if/core/net_stats.h"

#include "if/bridge/c2ui.h"

#include "lib/cq.h"
#include "lib/file.h"
#include "lib/glib-missing.h"
#include "lib/override.h"		/* Must be the last header included */

RCSID("$Id: settings.c,v 1.22 2005/05/14 04:11:04 cbiere Exp $");

#define debug dbg

static const gchar config_file[] = "config_gnet";
static const gchar ul_stats_file[] = "upload_stats";

#define CONFIG_DIR_MODE	/* 0755 */ \
	(S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)

static gchar *home_dir = NULL;
static gchar *config_dir = NULL;

static prop_set_t *properties = NULL;

/*
 * For backward compatibility these values are still read, but
 * no longer written to the config file:
 *
 * Variable                    Changed at       New name
 * ----------------            ---------------- -------------
 * socksv5_user                0.90u 12/05/2002 socks_user
 * socksv5_pass                0.90u 12/05/2002 socks_pass
 * progressbar_bps_in_visible  0.90u 15/05/2002 progressbar_bws_in_visible
 * progressbar_bps_out_visible 0.90u 15/05/2002 progressbar_bws_out_visible
 * progressbar_bps_in_avg      0.90u 15/05/2002 progressbar_bws_in_avg
 * progressbar_bps_out_avg     0.90u 15/05/2002 progressbar_bws_out_avg
 */

static const gchar pidfile[] = "gtk-gnutella.pid";

static void settings_callbacks_init(void);
static void settings_callbacks_shutdown(void);
static void update_uptimes(void);

/* ----------------------------------------- */

/*
 * ensure_unicity
 *
 * Look for any existing PID file. If found, look at the pid recorded
 * there and make sure it has died. Abort operations if it hasn't...
 */
static void
ensure_unicity(const gchar *file)
{
	FILE *f;
	pid_t pid;
	guint64 pid_value;
	gint error;
	gchar buf[16];

	f = file_fopen_missing(file, "r");
	if (f == NULL)
		return;				/* Assume it's missing if can't be opened */

	buf[0] = '\0';
	if (NULL == fgets(buf, sizeof(buf), f))
		return;
	fclose(f);

	pid_value = parse_uint64(buf, NULL, 10, &error);
	if (error)
		return;				/* Can't read it back correctly */

	pid = pid_value;

	/*
	 * Existence check relies on the existence of signal 0. The kernel
	 * won't actually send anything, but will perform all the existence
	 * checks inherent to the kill() syscall for us...
	 */

	if (-1 == kill(pid, 0)) {
		if (errno != ESRCH)
			g_warning("kill() return unexpected error: %s", g_strerror(errno));
		return;
	}

	fprintf(stderr,
		"You seem to have left another gtk-gnutella running (pid = %ld)\n",
		(glong) pid);
	exit(1);
}

/**
 * Write our pid to the pidfile.
 */
static void
save_pid(const gchar *file)
{
	FILE *f;

	f = file_fopen(file, "w");
	if (f == NULL)
		return;

	fprintf(f, "%lu\n", (gulong) getpid());

	if (0 != fclose(f))
		g_warning("could not flush pidfile \"%s\": %s",
			file, g_strerror(errno));
}

/* ----------------------------------------- */

#if !defined(_SC_PAGE_SIZE) && defined(_SC_PAGESIZE)
#define _SC_PAGE_SIZE _SC_PAGESIZE
#endif

#if defined (_SC_PAGE_SIZE) && defined (_SC_PHYS_PAGES)
static glong
settings_getpagesize(void)
{
	glong ret;

	errno = 0;
	ret = sysconf(_SC_PAGE_SIZE);
	if ((glong) -1 == ret && 0 != errno) {
		g_warning("sysconf(_SC_PHYS_PAGES) failed: %s", g_strerror(errno));
		return 0;
	}
	return ret;
}
#else
#define settings_getpagesize() ((glong) getpagesize())
#endif /* _SC_PAGE_SIZE && _SC_PHYS_PAGES */

/**
 * @return the amount of physical RAM in KB, or zero in case of failure
 */
static gulong
settings_getphysmemsize(void)
{
#if defined (_SC_PHYS_PAGES)
	guint32 pagesize = settings_getpagesize();
	glong pages;

	errno = 0;
	pages = sysconf(_SC_PHYS_PAGES);
	if ((glong) -1 == pages && 0 != errno) {
		g_warning("sysconf(_SC_PHYS_PAGES) failed: %s", g_strerror(errno));
		return 0;
	}
	return (pagesize >> 10) * (gulong) pages;

#elif defined (HAS_SYSCTL) && defined (CTL_HW) && defined (HW_USERMEM)
/* There's also HW_PHYSMEM but HW_USERMEM is better for our needs. */
	int mib[2] = { CTL_HW, HW_USERMEM };
	int amount = 0;
	size_t len = sizeof(amount);

	if (-1 == sysctl(mib, 2, &amount, &len, NULL, 0)) {
		g_warning(
			"settings_getphysmemsize: sysctl() for HW_USERMEM failed: %s",
			g_strerror(errno));
		return 0;
	}

	return amount / 1024;

#elif defined (HAS_GETINVENT)
	inventory_t *inv;
	long physmem = 0;

	if (-1 == setinvent()) {
		g_warning("settings_getphysmemsize: setinvent() failed: %s",
			g_strerror(errno));
		return 0;
	}

	errno = 0;
	while ((inv = getinvent()) != NULL) {
		if (inv->inv_class == INV_MEMORY && inv->inv_type == INV_MAIN_MB) {
			physmem = inv->inv_state;
			break;
		}
	}
	endinvent();

	if (-1L == physmem && 0 != errno) {
		g_warning("settings_getphysmemsize: "
			"getinvent() for INV_MEMORY faild: %s", g_strerror(errno));
		return 0;
	}

	return physmem * 1024;

#else /* ! _SC_PHYS_PAGES && ! HAS_SYSCTL && ! HAS_GETINVENT */

	g_warning("Unable to determine amount of physical RAM");
	return 0;

#endif /* _SC_PHYS_PAGES */
}

void
settings_init(void)
{
	guint32 maxfd = (guint32) sysconf(_SC_OPEN_MAX);
	gulong memory = settings_getphysmemsize();
	guint32 amount = (guint32) memory;
	struct rlimit lim;
	char *path = NULL;

	if (debug > 0) {
		g_message("detected amount of physical RAM: %lu KB", memory);
		g_message("process can use %u file descriptors", maxfd);
	}

	if (-1 != getrlimit(RLIMIT_DATA, &lim)) {
		guint32 maxdata = lim.rlim_cur >> 10;
		amount = MIN(amount, maxdata);		/* For our purposes */
	}

    properties = gnet_prop_init();

	gnet_prop_set_guint32_val(PROP_SYS_NOFILE, maxfd);
	gnet_prop_set_guint32_val(PROP_SYS_PHYSMEM, amount);

	memset(deconstify_gchar(servent_guid), 0, sizeof servent_guid);
	config_dir = g_strdup(getenv("GTK_GNUTELLA_DIR"));
	home_dir = g_strdup(eval_subst("~"));
	if (!home_dir)
		g_warning(_("Can't find your home directory!"));

	if (!config_dir && home_dir)
		config_dir = make_pathname(home_dir, ".gtk-gnutella");

	if (NULL == config_dir || '\0' == config_dir[0])
		goto no_config_dir;

	if (!is_directory(config_dir)) {
		g_warning(_("creating configuration directory \"%s\""), config_dir);

		if (mkdir(config_dir, CONFIG_DIR_MODE) == -1) {
			g_warning("mkdir(\"%s\") failed: \"%s\"",
				config_dir, g_strerror(errno));
			goto no_config_dir;
		}
	}

	g_assert(NULL != config_dir);
	/* Ensure we're the only instance running */

	path = make_pathname(config_dir, pidfile);
	ensure_unicity(path);
	save_pid(path);
	G_FREE_NULL(path);

		/* Parse the configuration */
	prop_load_from_file(properties, config_dir, config_file);

	path = make_pathname(config_dir, ul_stats_file);
	upload_stats_load_history(path);	/* Loads the upload statistics */
	G_FREE_NULL(path);


	/* watch for filter_file defaults */

	if (hard_ttl_limit < max_ttl) {
		*(guint32 *) &hard_ttl_limit = max_ttl;
		g_warning("hard_ttl_limit was too small, adjusted to %u",
			hard_ttl_limit);
	}

	/* Flow control depends on this being not too small */
	if (node_sendqueue_size < 1.5 * settings_max_msg_size()) {
		*(guint32 *) &node_sendqueue_size =
			(guint32) (1.5 * settings_max_msg_size());
		g_warning("node_sendqueue_size was too small, adjusted to %u",
			node_sendqueue_size);
	}

    settings_callbacks_init();
	return;

no_config_dir:
	g_warning(_("Cannot proceed without valid configuration directory"));
	exit(EXIT_FAILURE); /* g_error() would dump core, that's ugly. */
}

/**
 * Get the config directory
 */
const gchar *
settings_config_dir(void)
{
	g_assert(NULL != config_dir);
	return (const gchar *) config_dir;
}

/**
 * Gets the home dir.
 */
const gchar *
settings_home_dir(void)
{
	g_assert(NULL != home_dir);
	return (const gchar *) home_dir;
}

/**
 * Remove pidfile.
 */
static void
settings_remove_pidfile(void)
{
	gchar *path;

	path = make_pathname(config_dir, pidfile);
	g_return_if_fail(NULL != path);
	if (-1 == unlink(path))
		g_warning("could not remove pidfile \"%s\": %s",
			path, g_strerror(errno));
	G_FREE_NULL(path);
}

/**
 * This routine is called when we determined that our IP was no longer the
 * one we computed.  We base this on some headers sent back when we handshake
 * with other nodes, and as a result, cannot trust the information.
 *
 * What we do henceforth is trust 3 successive indication that our IP changed,
 * provided we get the same information each time.
 *
 *		--RAM, 13/01/2002
 *
 * ``peer_ip'' is the IP address of peer which reported the new IP address.
 * There must be 3 peers from 3 different /16 networks before a change is
 * accepted. Otherwise, it would be very easy to confuse GTKG by connecting
 * 3 times in a row and submitting a *wrong* IP address.
 *
 *		--cbiere, 2004-08-01
 */
void
settings_ip_changed(guint32 new_ip, guint32 peer_ip)
{
	static guint32 last_ip_seen = 0;
	static guint same_ip_count = 0;
	static guint32 peers[3];
	guint i;

	g_assert(!force_local_ip);		/* Must be called when IP isn't forced */
	g_assert(new_ip != 0);			/* The new IP must be valid */

	if (!ip_is_valid(new_ip) || !ip_is_valid(peer_ip))
		return;

	/*
	 * Don't accept updates for private addresses from non-private addresses
	 * and vice-versa.
	 */
	if (is_private_ip(new_ip) ^ is_private_ip(peer_ip))
		return;

	/*
	 * Accept updates for private addresses only from peer in the same /16
	 * network; addresses are in host byte order.
	 */
	if (
		is_private_ip(new_ip) &&
		is_private_ip(peer_ip) &&
		(peer_ip & 0xffff0000) != (new_ip & 0xffff0000)
	) {
		return;
	}

	peer_ip &= 0xffff0000;	/* One vote per /16 network; host byte order! */
	for (i = 0; i < G_N_ELEMENTS(peers); i++) {
		if (peers[i] == peer_ip)
			return;
	}

	if (new_ip != last_ip_seen) {
		last_ip_seen = new_ip;
		same_ip_count = 1;
		peers[0] = peer_ip;
		return;
	}

	g_assert(same_ip_count > 0 && same_ip_count < G_N_ELEMENTS(peers));
	peers[same_ip_count] = peer_ip;

	if (++same_ip_count < G_N_ELEMENTS(peers))
		return;

	last_ip_seen = 0;
	same_ip_count = 0;
	memset(peers, 0, sizeof peers);

	if (new_ip == local_ip)
		return;

    gnet_prop_set_guint32_val(PROP_LOCAL_IP, new_ip);
}

/**
 * Maximum message payload size we are configured to handle.
 */
guint32
settings_max_msg_size(void)
{
	/*
	 * Today, they are fixed at config time, but they will be set via
	 * GUI tomorrow, so the max size is not fixed in time.
	 *				--RAM, 15/09/2001
	 *
	 * They can be changed via the GUI as of today...
	 *				-- RAM, 24/12/2003
	 */

	guint32 maxsize;

	maxsize = MAX(search_queries_kick_size, search_answers_kick_size);
	maxsize = MAX(maxsize, other_messages_kick_size);

	return maxsize;
}

/**
 * Ask them to set a property to be able to run.
 */
void
settings_ask_for_property(gchar *name, gchar *value)
{
	fprintf(stderr, "\n*** ANCIENT VERSION DETECTED! ***\n\n");
	fprintf(stderr,
		"Sorry, this program is too ancient to run without\n"
		"an explicit user action: If it's not possible to upgrade\n"
		"you may edit the file\n\n"
		"\t%s%s%s\n\n"
		"and set the variable \"%s\" to\n\"%s\".\n\n"
		"You will then be able to run this version forever, but\n"
		"please consider upgrading, as Gnutella is an evolving\n"
		"network in which ancient software is less useful or even\n"
		"harmful!\n\n",
		config_dir, G_DIR_SEPARATOR_S, config_file, name, value);
	fprintf(stderr, "*** EXITING ***\n\n");
	exit(EXIT_FAILURE);
}

/**
 * Called at exit time to flush the property files.
 */
void
settings_shutdown(void)
{
	update_uptimes();
    settings_callbacks_shutdown();

    prop_save_to_file(properties, config_dir, config_file);
}

/**
 * Save settings if dirty.
 */
void
settings_save_if_dirty(void)
{
    prop_save_to_file_if_dirty(properties, config_dir, config_file);
}

/**
 * Finally free all memory allocated. Call after settings_shutdown.
 */
void
settings_close(void)
{
	settings_remove_pidfile();
    gnet_prop_shutdown();

	G_FREE_NULL(home_dir);
	G_FREE_NULL(config_dir);
}

void
gnet_get_bw_stats(gnet_bw_source type, gnet_bw_stats_t *s)
{
    g_assert(s != NULL);

    switch (type) {
    case BW_GNET_IN:
        s->enabled  = bws_gin_enabled;
        s->current  = bsched_bps(bws.gin);
        s->average  = bsched_avg_bps(bws.gin);
        s->limit    = bws.gin->bw_per_second;
        break;
    case BW_GNET_OUT:
        s->enabled  = bws_gout_enabled;
        s->current  = bsched_bps(bws.gout);
        s->average  = bsched_avg_bps(bws.gout);
        s->limit    = bws.gout->bw_per_second;
        break;
    case BW_GNET_UDP_IN:
        s->enabled  = bws_gin_enabled;
        s->current  = bsched_bps(bws.gin_udp);
        s->average  = bsched_avg_bps(bws.gin_udp);
        s->limit    = bws.gin_udp->bw_per_second;
        break;
    case BW_GNET_UDP_OUT:
        s->enabled  = bws_gout_enabled;
        s->current  = bsched_bps(bws.gout_udp);
        s->average  = bsched_avg_bps(bws.gout_udp);
        s->limit    = bws.gout_udp->bw_per_second;
        break;
    case BW_HTTP_IN:
        s->enabled  = bws_in_enabled;
        s->current  = bsched_bps(bws.in);
        s->average  = bsched_avg_bps(bws.in);
        s->limit    = bws.in->bw_per_second;
        break;
    case BW_HTTP_OUT:
        s->enabled  = bws_out_enabled;
        s->current  = bsched_bps(bws.out);
        s->average  = bsched_avg_bps(bws.out);
        s->limit    = bws.out->bw_per_second;
        break;
    case BW_LEAF_IN:
        s->enabled  = bws_glin_enabled;
        s->current  = bsched_bps(bws.glin);
        s->average  = bsched_avg_bps(bws.glin);
        s->limit    = bws.glin->bw_per_second;
        break;
    case BW_LEAF_OUT:
        s->enabled  = bws_glout_enabled;
        s->current  = bsched_bps(bws.glout);
        s->average  = bsched_avg_bps(bws.glout);
        s->limit    = bws.glout->bw_per_second;
        break;
    }
}

/***
 *** Internal helpers.
 ***/

/**
 * Compute the EMA of the IP address lifetime up to now, but do not
 * update the property.
 */
guint32
get_average_ip_lifetime(time_t now)
{
	guint32 current_ip_stamp;
	guint32 average_ip_uptime;
	gint32 lifetime;

	gnet_prop_get_guint32_val(PROP_CURRENT_IP_STAMP, &current_ip_stamp);
	gnet_prop_get_guint32_val(PROP_AVERAGE_IP_UPTIME, &average_ip_uptime);

	if (current_ip_stamp) {
		lifetime = delta_time(now, current_ip_stamp);
		if (lifetime < 0)
			lifetime = 0;
	} else
		lifetime = 0;

	/*
	 * The average lifetime is computed as an EMA on 3 terms.
	 * The smoothing factor sm=2/(3+1) is therefore 0.5.
	 */

	average_ip_uptime += (lifetime >> 1) - (average_ip_uptime >> 1);

	return average_ip_uptime;
}

/**
 * Called whenever the IP address we advertise changed.
 * Update the average uptime for a given IP address.
 */
static void
update_address_lifetime(void)
{
	static guint32 old_ip = 0;
	gboolean force_local_ip;
	guint32 current_ip;
	guint32 current_ip_stamp;
	time_t now;

	gnet_prop_get_boolean_val(PROP_FORCE_LOCAL_IP, &force_local_ip);
	if (force_local_ip)
		gnet_prop_get_guint32_val(PROP_FORCED_LOCAL_IP, &current_ip);
	else
		gnet_prop_get_guint32_val(PROP_LOCAL_IP, &current_ip);

	if (old_ip == 0) {				/* First time */
		old_ip = current_ip;
		gnet_prop_get_guint32_val(PROP_CURRENT_IP_STAMP, &current_ip_stamp);
		if (current_ip_stamp == 0) {
			now = time(NULL);
			gnet_prop_set_guint32_val(PROP_CURRENT_IP_STAMP, (guint32) now);
		}
	}

	if (old_ip == current_ip)
		return;

	/*
	 * IP address changed, update lifetime information.
	 */

	now = time(NULL);
	old_ip = current_ip;

	gnet_prop_get_guint32_val(PROP_CURRENT_IP_STAMP, &current_ip_stamp);

	if (current_ip_stamp)
		gnet_prop_set_guint32_val(PROP_AVERAGE_IP_UPTIME,
			get_average_ip_lifetime(now));

	gnet_prop_set_guint32_val(PROP_CURRENT_IP_STAMP, (guint32) now);
}

/**
 * Compute the EMA of the averate servent uptime, up to now, but do not
 * update the property.
 */
guint32
get_average_servent_uptime(time_t now)
{
	time_t start_stamp;
	gint32 uptime;
	guint32 val, avg_servent_uptime;

	gnet_prop_get_guint32_val(PROP_AVERAGE_SERVENT_UPTIME, &avg_servent_uptime);
	gnet_prop_get_guint32_val(PROP_START_STAMP, &val);

	start_stamp = val;
	uptime = delta_time(now, start_stamp);
	if (uptime < 0)
		uptime = 0;

	/*
	 * The average uptime is computed as an EMA on 7 terms.
	 * The smoothing factor sm=2/(7+1) is therefore 0.25.
	 */

	avg_servent_uptime += (uptime >> 2) - (avg_servent_uptime >> 2);

	return avg_servent_uptime;
}

/**
 * Called at shutdown time to update the average_uptime property before
 * saving the properties to disk.
 */
static void
update_uptimes(void)
{
	time_t now = time(NULL);

	gnet_prop_set_guint32_val(PROP_AVERAGE_SERVENT_UPTIME,
		get_average_servent_uptime(now));

	gnet_prop_set_guint32_val(PROP_AVERAGE_IP_UPTIME,
		get_average_ip_lifetime(now));
}

/***
 *** Callbacks
 ***/
static gboolean
up_connections_changed(property_t prop)
{
    guint32 up_connections;
    guint32 max_connections;

	g_assert(PROP_UP_CONNECTIONS == prop);
    gnet_prop_get_guint32_val(prop, &up_connections);
    gnet_prop_get_guint32_val(PROP_MAX_CONNECTIONS, &max_connections);

    if (up_connections > max_connections)
        gnet_prop_set_guint32_val(PROP_MAX_CONNECTIONS, up_connections);

    return FALSE;
}

static gboolean
max_connections_changed(property_t prop)
{
    guint32 up_connections;
    guint32 max_connections;

	g_assert(PROP_MAX_CONNECTIONS == prop);
    gnet_prop_get_guint32_val(prop, &max_connections);
    gnet_prop_get_guint32_val(PROP_UP_CONNECTIONS, &up_connections);

    if (up_connections > max_connections)
        gnet_prop_set_guint32_val(PROP_UP_CONNECTIONS, max_connections);

    return FALSE;
}

static gboolean
max_hosts_cached_changed(property_t prop)
{
	g_assert(PROP_MAX_HOSTS_CACHED == prop);
    hcache_prune(HCACHE_FRESH_ANY);

    return FALSE;
}

static gboolean
max_ultra_hosts_cached_changed(property_t prop)
{
	g_assert(PROP_MAX_ULTRA_HOSTS_CACHED == prop);
    hcache_prune(HCACHE_FRESH_ULTRA);

    return FALSE;
}

static gboolean
max_bad_hosts_cached_changed(property_t prop)
{
	g_assert(PROP_MAX_BAD_HOSTS_CACHED == prop);
    hcache_prune(HCACHE_BUSY);
    hcache_prune(HCACHE_TIMEOUT);
    hcache_prune(HCACHE_UNSTABLE);

    return FALSE;
}

static gboolean
enable_udp_changed(property_t prop)
{
	gboolean enabled;

    gnet_prop_get_boolean_val(prop, &enabled);
	if (enabled) {
		if (s_udp_listen == NULL && listen_port)
			s_udp_listen = socket_udp_listen(0, listen_port);
	} else {
		if (s_udp_listen) {
			socket_free(s_udp_listen);
			s_udp_listen = NULL;
		}
	}
	node_update_udp_socket();

	return FALSE;
}

static gboolean
listen_port_changed(property_t prop)
{
	static guint32 old_listen_port = (guint32) -1;
	gboolean random_port = FALSE;
    guint32 listen_port;
	guint num_tried = 0;
	guint32 tried[65536 / (8 * sizeof(guint32))]; /* Use bits as bool flags */

    gnet_prop_get_guint32_val(prop, &listen_port);

	/*
	 * If port did not change values, do nothing.
	 */

	if (listen_port == old_listen_port && listen_port != 0)
		return FALSE;

	if (old_listen_port != (guint32) -1) {
		inet_firewalled();			/* Assume we're firewalled on port change */
		inet_udp_firewalled();
	}

	random_port = listen_port == 1;
	memset(tried, 0, sizeof tried);
	memset(tried, 0xff, 1024 / 8); /* Mark ports below 1024 as already tried */

	do {
		if (random_port) {
			guint32 i, b, r;

			listen_port = r = random_value(65535 - 1024) + 1024;
			/* Check whether this port was tried before */
			do {
				i = r / (8 * sizeof tried[0]);
				b = 1 << (r % (8 * sizeof tried[0]));
				if ((tried[i] & b) == 0) {
					tried[i] |= b;
					listen_port = r;
					break;
				}
				r = (r + 101) & 0xffff;
			} while (r != listen_port);

			g_assert(listen_port > 1023);
		}

		old_listen_port = listen_port;

		/*
		 * Close old ports.
		 */

		if (s_tcp_listen) {
			socket_free(s_tcp_listen);
			s_tcp_listen = NULL;
		}

		if (s_udp_listen) {
			socket_free(s_udp_listen);
			s_udp_listen = NULL;
		}

		/*
		 * If the new port != 0, open the new port
		 */

		if (listen_port)
			s_tcp_listen = socket_tcp_listen(0, listen_port, SOCK_TYPE_CONTROL);

		/*
		 * If UDP is enabled, also listen on the same UDP port.
		 */

		if (enable_udp) {
			if (listen_port)
				s_udp_listen = socket_udp_listen(0, listen_port);
			if (random_port && s_udp_listen == NULL) {
				socket_free(s_tcp_listen);
				s_tcp_listen = NULL;
			}
			node_update_udp_socket();
		}
	} while (random_port && s_tcp_listen == NULL && ++num_tried < 65535 - 1024);

    /*
     * If socket allocation failed, reset the property
     */

    if ((s_tcp_listen == NULL) && (listen_port != 0)) {
		if (random_port) {
			old_listen_port = (guint32) -1;
			listen_port = 0;
		} else
        	old_listen_port = listen_port = 0;

        gnet_prop_set_guint32_val(prop, listen_port);
        return TRUE;
    }

	if (random_port)
		gnet_prop_set_guint32_val(prop, listen_port);

    return FALSE;
}

static gboolean
bw_http_in_enabled_changed(property_t prop)
{
    gboolean val;

    gnet_prop_get_boolean_val(prop, &val);
    if (val)
        bsched_enable(bws.in);
    else
        bsched_disable(bws.in);

    return FALSE;
}

static gboolean
bw_http_out_enabled_changed(property_t prop)
{
    gboolean val;

    gnet_prop_get_boolean_val(prop, &val);
    if (val)
        bsched_enable(bws.out);
    else
        bsched_disable(bws.out);

    return FALSE;
}

static gboolean
bw_gnet_in_enabled_changed(property_t prop)
{
    gboolean val;

    gnet_prop_get_boolean_val(prop, &val);
    if (val)
        bsched_enable(bws.gin);
    else
        bsched_disable(bws.gin);

    return FALSE;
}

static gboolean
bw_gnet_out_enabled_changed(property_t prop)
{
    gboolean val;

    gnet_prop_get_boolean_val(prop, &val);
    if (val)
        bsched_enable(bws.gout);
    else
        bsched_disable(bws.gout);

    return FALSE;
}

static gboolean
bw_gnet_lin_enabled_changed(property_t prop)
{
    gboolean val;

    gnet_prop_get_boolean_val(prop, &val);
    if (val)
        bsched_enable(bws.glin);
    else
        bsched_disable(bws.glin);

    return FALSE;
}

static gboolean
bw_gnet_lout_enabled_changed(property_t prop)
{
    gboolean val;

    gnet_prop_get_boolean_val(prop, &val);
    if (val)
        bsched_enable(bws.glout);
    else
        bsched_disable(bws.glout);

    return FALSE;
}

static gboolean
node_sendqueue_size_changed(property_t unused_prop)
{
    guint32 val;
    guint32 min = 1.5 * settings_max_msg_size();

	(void) unused_prop;
    gnet_prop_get_guint32_val(PROP_NODE_SENDQUEUE_SIZE, &val);
    if (val < min) {
        gnet_prop_set_guint32_val(PROP_NODE_SENDQUEUE_SIZE, min);
        return TRUE;
    }

    return FALSE;
}

static gboolean
scan_extensions_changed(property_t prop)
{
    gchar *s = gnet_prop_get_string(prop, NULL, 0);

    parse_extensions(s);
    G_FREE_NULL(s);

    return FALSE;
}

static gboolean
file_path_changed(property_t prop)
{
    gchar *s = gnet_prop_get_string(prop, NULL, 0);

	g_assert(s != NULL);

	if (!is_directory(s)) {
		g_message("Attempt to create directory \"%s\"", s);
		if (create_directory(s))
			g_message("Attempt failed: \"%s\"", g_strerror(errno));
	}

    if (!is_directory(s)) {
		gchar *path;

        if (prop == PROP_SAVE_FILE_PATH) {
			prop_def_t *def = gnet_prop_get_def(prop);
			path = g_strdup(eval_subst(*def->data.string.def));
			prop_free_def(def);
		} else
			path = gnet_prop_get_string(PROP_SAVE_FILE_PATH, NULL, 0);

        g_warning("property \"%s\": directory %s is not available, "
            "using %s instead", gnet_prop_name(prop), s, path);

        gnet_prop_set_string(prop, path);

		G_FREE_NULL(path);
        G_FREE_NULL(s);
        return TRUE;
    }

    G_FREE_NULL(s);
    return FALSE;
}

static gboolean
shared_dirs_paths_changed(property_t prop)
{
    gchar *s = gnet_prop_get_string(prop, NULL, 0);
	gboolean ok;

	ok = shared_dirs_parse(s);
	G_FREE_NULL(s);

	if (!ok) {
		shared_dirs_update_prop();
		return TRUE;
	}

	return FALSE;
}

static gboolean
local_netmasks_string_changed(property_t prop)
{
    gchar *s = gnet_prop_get_string(prop, NULL, 0);

    parse_netmasks(s);
    G_FREE_NULL(s);

    return FALSE;
}

static gboolean
hard_ttl_limit_changed(property_t prop)
{
    guint32 hard_ttl_limit;
    guint32 max_ttl;

	g_assert(PROP_HARD_TTL_LIMIT == prop);
    gnet_prop_get_guint32_val(prop, &hard_ttl_limit);
    gnet_prop_get_guint32_val(PROP_MAX_TTL, &max_ttl);

    if (hard_ttl_limit < max_ttl)
        gnet_prop_set_guint32_val(PROP_MAX_TTL, hard_ttl_limit);

    return FALSE;
}

static gboolean
max_ttl_changed(property_t prop)
{
    guint32 hard_ttl_limit;
    guint32 max_ttl;

	g_assert(PROP_MAX_TTL == prop);
    gnet_prop_get_guint32_val(prop, &max_ttl);
    gnet_prop_get_guint32_val(PROP_HARD_TTL_LIMIT, &hard_ttl_limit);

    if (hard_ttl_limit < max_ttl)
        gnet_prop_set_guint32_val(PROP_HARD_TTL_LIMIT, max_ttl);

    return FALSE;
}

static gboolean
bw_http_in_changed(property_t prop)
{
    guint32 val;

	g_assert(PROP_BW_HTTP_IN == prop);
    gnet_prop_get_guint32_val(prop, &val);
    bsched_set_bandwidth(bws.in, val);

	gnet_prop_get_guint32_val(PROP_CURRENT_PEERMODE, &val);
	bsched_set_peermode(val);

    return FALSE;
}

static gboolean
bw_http_out_changed(property_t prop)
{
    guint32 val;

    gnet_prop_get_guint32_val(prop, &val);
    bsched_set_bandwidth(bws.out, val);

	gnet_prop_get_guint32_val(PROP_CURRENT_PEERMODE, &val);
	bsched_set_peermode(val);

    return FALSE;
}

static gboolean
bw_gnet_in_changed(property_t prop)
{
    guint32 val;

    gnet_prop_get_guint32_val(prop, &val);
    bsched_set_bandwidth(bws.gin, val / 2);
    bsched_set_bandwidth(bws.gin_udp, val / 2);

	gnet_prop_get_guint32_val(PROP_CURRENT_PEERMODE, &val);
	bsched_set_peermode(val);

    return FALSE;
}

static gboolean
bw_gnet_out_changed(property_t prop)
{
    guint32 val;

    gnet_prop_get_guint32_val(prop, &val);
    bsched_set_bandwidth(bws.gout, val / 2);
    bsched_set_bandwidth(bws.gout_udp, val / 2);

	gnet_prop_get_guint32_val(PROP_CURRENT_PEERMODE, &val);
	bsched_set_peermode(val);

    return FALSE;
}

static gboolean
bw_gnet_lin_changed(property_t prop)
{
    guint32 val;

    gnet_prop_get_guint32_val(prop, &val);
    bsched_set_bandwidth(bws.glin, val);

	gnet_prop_get_guint32_val(PROP_CURRENT_PEERMODE, &val);
	bsched_set_peermode(val);

    return FALSE;
}

static gboolean
bw_gnet_lout_changed(property_t prop)
{
    guint32 val;

    gnet_prop_get_guint32_val(prop, &val);
    bsched_set_bandwidth(bws.glout, val);

	gnet_prop_get_guint32_val(PROP_CURRENT_PEERMODE, &val);
	bsched_set_peermode(val);

    return FALSE;
}

static gboolean
bw_allow_stealing_changed(property_t prop)
{
	gboolean val;

	gnet_prop_get_boolean_val(prop, &val);

	if (val)
		bsched_config_steal_http_gnet();
	else
		bsched_config_steal_gnet();

	return FALSE;
}

static gboolean
node_online_mode_changed(property_t prop)
{
	gboolean val;

	gnet_prop_get_boolean_val(prop, &val);
	node_set_online_mode(val);

    return FALSE;
}

static gboolean
lib_debug_changed(property_t unused_prop)
{
	(void) unused_prop;
/* XXX -- common_dbg is no longer a property! --RAM */
#if 0
	gnet_prop_get_guint32_val(prop, &common_dbg);
#endif
    return FALSE;
}

static gboolean
force_local_ip_changed(property_t prop)
{
	g_assert(PROP_FORCE_LOCAL_IP == prop);
	update_address_lifetime();
    return FALSE;
}

static gboolean
local_ip_changed(property_t prop)
{
	g_assert(PROP_LOCAL_IP == prop);
	update_address_lifetime();
    return FALSE;
}

static gboolean
configured_peermode_changed(property_t prop)
{
    guint32 val;
	gboolean forced = FALSE;

    gnet_prop_get_guint32_val(prop, &val);

	/* XXX: The following is disabled because it is too restrictive and
	 *		annoying in LAN. If a user doesn't use the default "auto"
	 *		mode, it can be assumed that he knows what he's doing. Also,
	 *		while it's sub-optimal it's not absolutely required for an
	 *		ultrapeer to accept incoming connections (from external hosts).
	 * 
	 *		--cbiere, 2005-05-14
	 */
#if 0
	/*
	 * We don't allow them to be anything but a leaf node if they are
	 * firewalled.  We even restrict the "normal" mode, which is to be
	 * avoided anyway, and will be removed in a future release.
	 *		--RAM, 2004-09-19
	 */

	switch (val) {
	case NODE_P_NORMAL:
	case NODE_P_ULTRA:
		if (is_firewalled) {
			val = NODE_P_AUTO;
			forced = TRUE;
			g_warning("must run as a leaf when TCP-firewalled");
			gcu_statusbar_warning(
				_("Can only run as a leaf when TCP-firewalled"));
		}
		break;
	default:
		break;
	}
#endif

	if (val == NODE_P_AUTO) {
		if (connected_nodes() > 0)		/* Already connected */
			return forced;				/* Keep our current operating mode */
		val = NODE_P_LEAF;				/* Force leaf mode */
		/* FALL THROUGH */
	}

	gnet_prop_set_guint32_val(PROP_CURRENT_PEERMODE, val);

    return forced;
}

static gboolean
current_peermode_changed(property_t prop)
{
    guint32 val;

    gnet_prop_get_guint32_val(prop, &val);
	node_current_peermode_changed((node_peer_t) val);

    return FALSE;
}

static gboolean
download_rx_size_changed(property_t prop)
{
    guint32 val;

    gnet_prop_get_guint32_val(prop, &val);
	download_set_socket_rx_size(val * 1024);

	return FALSE;
}

static gboolean
node_rx_size_changed(property_t prop)
{
    guint32 val;

    gnet_prop_get_guint32_val(prop, &val);
	node_set_socket_rx_size(val * 1024);

	return FALSE;
}

/*
 * Automatically reset properties have a callout queue entry associated
 * with them.  When the entry fires, the property is cleared.  Each time
 * the property is set, the callout entry is reactivated some time in the
 * future.
 */

static gpointer ev_file_descriptor_shortage = NULL;
static gpointer ev_file_descriptor_runout = NULL;

#define RESET_PROP_TM	(10*60*1000)	/* 10 minutes in ms */

/**
 * callout queue callback
 *
 * Reset the property.
 */
static void
reset_property_cb(cqueue_t *unused_cq, gpointer obj)
{
	property_t prop = (property_t) GPOINTER_TO_UINT(obj);

	(void) unused_cq;
	switch (prop) {
	case PROP_FILE_DESCRIPTOR_SHORTAGE:
		ev_file_descriptor_shortage = NULL;
		break;
	case PROP_FILE_DESCRIPTOR_RUNOUT:
		ev_file_descriptor_runout = NULL;
		break;
	default:
		g_error("unhandled property #%d", prop);
		break;
	}

	gnet_prop_set_boolean_val(prop, FALSE);
}

static gboolean
file_descriptor_x_changed(property_t prop)
{
	gboolean state;
	gpointer *ev = NULL;

	gnet_prop_get_boolean_val(prop, &state);
	if (!state)
		return FALSE;

	/*
	 * Property is set to TRUE: arm callback to reset it in 10 minutes.
	 */

	switch (prop) {
	case PROP_FILE_DESCRIPTOR_SHORTAGE:
		ev = &ev_file_descriptor_shortage;
		break;
	case PROP_FILE_DESCRIPTOR_RUNOUT:
		ev = &ev_file_descriptor_runout;
		break;
	default:
		g_error("unhandled property #%d", prop);
		break;
	}

	g_assert(ev != NULL);

	if (*ev == NULL)
		*ev = cq_insert(callout_queue, RESET_PROP_TM, reset_property_cb,
			GUINT_TO_POINTER(prop));
	else
		cq_resched(callout_queue, *ev, RESET_PROP_TM);

    return FALSE;
}

/**
 * This is only necessary to migrate the old PROP_PROXY_IP to
 * PROP_PROXY_HOSTNAME and should be removed in a future release.
 *    -- cbiere, 2004-06-29
 */
static gboolean
proxy_ip_changed(property_t prop)
{
	guint32 ip;

	gnet_prop_get_guint32_val(prop, &ip);
	if (ip) {
		gchar *hostname = gnet_prop_get_string(PROP_PROXY_HOSTNAME, NULL, 0);

		if (hostname[0] == '\0')
			gnet_prop_set_string(PROP_PROXY_HOSTNAME, ip_to_gchar(ip));
    	G_FREE_NULL(hostname);
	}
    return FALSE;
}

/***
 *** Property-to-callback map
 ***/

typedef struct prop_map {
    property_t prop;            /* property handle */
    prop_changed_listener_t cb; /* callback function */
    gboolean init;              /* init widget with current value */
} prop_map_t;

#define IGNORE NULL

static prop_map_t property_map[] = {
    {
        PROP_NODE_SENDQUEUE_SIZE,
        node_sendqueue_size_changed,
        TRUE
    },
    {
        PROP_SEARCH_QUERIES_KICK_SIZE,
        node_sendqueue_size_changed,
        TRUE
    },
    {
        PROP_SEARCH_ANSWERS_KICK_SIZE,
        node_sendqueue_size_changed,
        TRUE
    },
    {
        PROP_UP_CONNECTIONS,
        up_connections_changed,
        TRUE
    },
    {
        PROP_MAX_CONNECTIONS,
        max_connections_changed,
        TRUE
    },
    {
        PROP_MAX_HOSTS_CACHED,
        max_hosts_cached_changed,
        TRUE
    },
    {
        PROP_MAX_ULTRA_HOSTS_CACHED,
        max_ultra_hosts_cached_changed,
        TRUE
	},
    {
        PROP_MAX_BAD_HOSTS_CACHED,
        max_bad_hosts_cached_changed,
        TRUE
	},
    {
        PROP_LISTEN_PORT,
        listen_port_changed,
        TRUE
    },
    {
        PROP_BW_HTTP_IN_ENABLED,
        bw_http_in_enabled_changed,
        FALSE
    },
    {
        PROP_BW_HTTP_OUT_ENABLED,
        bw_http_out_enabled_changed,
        FALSE
    },
    {
        PROP_BW_GNET_IN_ENABLED,
        bw_gnet_in_enabled_changed,
        FALSE
    },
    {
        PROP_BW_GNET_OUT_ENABLED,
        bw_gnet_out_enabled_changed,
        FALSE
    },
    {
        PROP_BW_GNET_LEAF_IN_ENABLED,
        bw_gnet_lin_enabled_changed,
        FALSE
    },
    {
        PROP_BW_GNET_LEAF_OUT_ENABLED,
        bw_gnet_lout_enabled_changed,
        FALSE
    },
    {
        PROP_SCAN_EXTENSIONS,
        scan_extensions_changed,
        TRUE
    },
    {
        PROP_SAVE_FILE_PATH,
        file_path_changed,
        TRUE
    },
    {
        PROP_MOVE_FILE_PATH,
        file_path_changed,
        TRUE
    },
    {
        PROP_BAD_FILE_PATH,
        file_path_changed,
        TRUE
    },
    {
        PROP_SHARED_DIRS_PATHS,
        shared_dirs_paths_changed,
        TRUE
    },
    {
        PROP_LOCAL_NETMASKS_STRING,
        local_netmasks_string_changed,
        TRUE
    },
    {
        PROP_HARD_TTL_LIMIT,
        hard_ttl_limit_changed,
        TRUE
    },
    {
        PROP_MAX_TTL,
        max_ttl_changed,
        TRUE
    },
    {
        PROP_BW_HTTP_IN,
        bw_http_in_changed,
        FALSE
    },
    {
        PROP_BW_HTTP_OUT,
        bw_http_out_changed,
        FALSE
    },
    {
        PROP_BW_GNET_IN,
        bw_gnet_in_changed,
        FALSE
    },
    {
        PROP_BW_GNET_OUT,
        bw_gnet_out_changed,
        FALSE
    },
    {
        PROP_BW_GNET_LIN,
        bw_gnet_lin_changed,
        FALSE
    },
    {
        PROP_BW_GNET_LOUT,
        bw_gnet_lout_changed,
        FALSE
    },
    {
        PROP_BW_ALLOW_STEALING,
        bw_allow_stealing_changed,
        FALSE
    },
	{
		PROP_ONLINE_MODE,
		node_online_mode_changed,
		TRUE						/* Need to call callback at init time */
	},
    {
        PROP_LIB_DEBUG,
        lib_debug_changed,
        TRUE
    },
	{
		PROP_FORCE_LOCAL_IP,
		force_local_ip_changed,
		TRUE,
	},
	{
		PROP_LOCAL_IP,
		local_ip_changed,
		TRUE,
	},
	{
		PROP_CONFIGURED_PEERMODE,
		configured_peermode_changed,
		TRUE,
	},
	{
		PROP_CURRENT_PEERMODE,
		current_peermode_changed,
		TRUE,
	},
	{
		PROP_DOWNLOAD_RX_SIZE,
		download_rx_size_changed,
		TRUE,
	},
	{
		PROP_NODE_RX_SIZE,
		node_rx_size_changed,
		TRUE,
	},
	{
		PROP_FILE_DESCRIPTOR_SHORTAGE,
		file_descriptor_x_changed,
		FALSE,
	},
	{
		PROP_FILE_DESCRIPTOR_RUNOUT,
		file_descriptor_x_changed,
		FALSE,
	},
	{
		PROP_PROXY_IP,
		proxy_ip_changed,
		TRUE,
	},
	{
		PROP_ENABLE_UDP,
		enable_udp_changed,
		FALSE,				/* UDP socket inited via listen_port_changed() */
	},
};

/***
 *** Control functions
 ***/

#define PROPERTY_MAP_SIZE G_N_ELEMENTS(property_map)

static gboolean init_list[GNET_PROPERTY_NUM];

static void
settings_callbacks_init(void)
{
    guint n;

    for (n = 0; n < GNET_PROPERTY_NUM; n ++)
        init_list[n] = FALSE;

    if (debug >= 2) {
        printf("settings_callbacks_init: property_map size: %u\n",
            (guint) PROPERTY_MAP_SIZE);
    }

    for (n = 0; n < PROPERTY_MAP_SIZE; n ++) {
        property_t prop = property_map[n].prop;
        guint32 idx = prop - GNET_PROPERTY_MIN;

        if (!init_list[idx])
           init_list[idx] = TRUE;
        else
            g_warning("settings_callbacks_init:"
                " property %d already mapped", n);

        if (property_map[n].cb != IGNORE) {
            gnet_prop_add_prop_changed_listener(
                property_map[n].prop,
                property_map[n].cb,
                property_map[n].init);
        } else if (debug >= 10) {
            printf("settings_callbacks_init: property ignored: %s\n",
				gnet_prop_name(prop));
        }
    }

    if (debug >= 1) {
        for (n = 0; n < GNET_PROPERTY_NUM; n++) {
            if (!init_list[n])
                printf("settings_callbacks_init: unmapped property: %s\n",
					gnet_prop_name(n+GNET_PROPERTY_MIN));
        }
    }
}

static void
settings_callbacks_shutdown(void)
{
    guint n;

	if (ev_file_descriptor_shortage != NULL) {
		cq_cancel(callout_queue, ev_file_descriptor_shortage);
		ev_file_descriptor_shortage = NULL;
	}
	if (ev_file_descriptor_runout != NULL) {
		cq_cancel(callout_queue, ev_file_descriptor_runout);
		ev_file_descriptor_runout = NULL;
	}

    for (n = 0; n < PROPERTY_MAP_SIZE; n ++) {
        if (property_map[n].cb != IGNORE) {
            gnet_prop_remove_prop_changed_listener(
                property_map[n].prop,
                property_map[n].cb);
        }
    }
}

/* vi: set ts=4 sw=4 cindent: */
