[telepathy-gabble/telepathy-gabble-0.8] make caps cache persist to disk using SQLite
Dafydd Harries
dafydd.harries at collabora.co.uk
Thu Jan 7 18:14:23 PST 2010
---
configure.ac | 5 +
src/Makefile.am | 4 +-
src/caps-cache.c | 481 ++++++++++++++++++++++++++-
src/caps-cache.h | 2 +-
tests/twisted/Makefile.am | 1 +
tests/twisted/caps/caps-persistent-cache.py | 81 +++++
tests/twisted/tools/exec-with-log.sh.in | 1 +
7 files changed, 556 insertions(+), 19 deletions(-)
create mode 100644 tests/twisted/caps/caps-persistent-cache.py
diff --git a/configure.ac b/configure.ac
index b018641..c7e2a94 100644
--- a/configure.ac
+++ b/configure.ac
@@ -165,6 +165,11 @@ AC_CHECK_FUNC(res_query, ,
AC_SUBST(RESOLV_LIBS)
+PKG_CHECK_MODULES(SQLITE, [sqlite3])
+
+AC_SUBST(SQLITE_CFLAGS)
+AC_SUBST(SQLITE_LIBS)
+
dnl Check for code generation tools
XSLTPROC=
AC_CHECK_PROGS([XSLTPROC], [xsltproc])
diff --git a/src/Makefile.am b/src/Makefile.am
index ddaa348..5a81d32 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -169,12 +169,12 @@ noinst_LTLIBRARIES = libgabble-convenience.la
AM_CFLAGS = $(ERROR_CFLAGS) -I$(top_srcdir) -I$(top_builddir) \
@DBUS_CFLAGS@ @GLIB_CFLAGS@ @LOUDMOUTH_CFLAGS@ \
@HANDLE_LEAK_DEBUG_CFLAGS@ @TP_GLIB_CFLAGS@ \
- @SOUP_CFLAGS@ @UUID_CFLAGS@ \
+ @SOUP_CFLAGS@ @UUID_CFLAGS@ @SQLITE_CFLAGS@ \
-I $(top_srcdir)/lib -I $(top_builddir)/lib \
-DG_LOG_DOMAIN=\"gabble\"
ALL_LIBS = @DBUS_LIBS@ @GLIB_LIBS@ @LOUDMOUTH_LIBS@ @TP_GLIB_LIBS@ \
- @SOUP_LIBS@ @UUID_LIBS@
+ @SOUP_LIBS@ @UUID_LIBS@ @SQLITE_LIBS@
# build gibber first
all: gibber
diff --git a/src/caps-cache.c b/src/caps-cache.c
index 417396e..d8b51fe 100644
--- a/src/caps-cache.c
+++ b/src/caps-cache.c
@@ -1,6 +1,16 @@
+#include "config.h"
#include "caps-cache.h"
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sqlite3.h>
+
+#define DEBUG_FLAG GABBLE_DEBUG_PRESENCE
+#include "debug.h"
+
G_DEFINE_TYPE (GabbleCapsCache, gabble_caps_cache, G_TYPE_OBJECT)
#define GET_PRIVATE(o) \
@@ -10,15 +20,31 @@ static GabbleCapsCache *shared_cache = NULL;
struct _GabbleCapsCachePrivate
{
- GHashTable *cache;
+ gchar *path;
+ sqlite3 *db;
+ guint inserts;
+};
+
+enum
+{
+ PROP_PATH = 1,
};
+static GObject *
+gabble_caps_cache_constructor (
+ GType type, guint n_props, GObjectConstructParam *props);
+
static void
gabble_caps_cache_get_property (GObject *object, guint property_id,
GValue *value, GParamSpec *pspec)
{
+ GabbleCapsCache *self = (GabbleCapsCache *) object;
+
switch (property_id)
{
+ case PROP_PATH:
+ g_value_set_string (value, self->priv->path);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
@@ -28,8 +54,14 @@ static void
gabble_caps_cache_set_property (GObject *object, guint property_id,
const GValue *value, GParamSpec *pspec)
{
+ GabbleCapsCache *self = (GabbleCapsCache *) object;
+
switch (property_id)
{
+ case PROP_PATH:
+ g_free (self->priv->path);
+ self->priv->path = g_value_dup_string (value);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
@@ -44,6 +76,17 @@ gabble_caps_cache_dispose (GObject *object)
static void
gabble_caps_cache_finalize (GObject *object)
{
+ GabbleCapsCache *self = GABBLE_CAPS_CACHE (object);
+
+ g_free (self->priv->path);
+ self->priv->path = NULL;
+
+ if (self->priv->db != NULL)
+ {
+ sqlite3_close (self->priv->db);
+ self->priv->db = NULL;
+ }
+
G_OBJECT_CLASS (gabble_caps_cache_parent_class)->finalize (object);
}
@@ -54,10 +97,125 @@ gabble_caps_cache_class_init (GabbleCapsCacheClass *klass)
g_type_class_add_private (klass, sizeof (GabbleCapsCachePrivate));
+ object_class->constructor = gabble_caps_cache_constructor;
object_class->get_property = gabble_caps_cache_get_property;
object_class->set_property = gabble_caps_cache_set_property;
object_class->dispose = gabble_caps_cache_dispose;
object_class->finalize = gabble_caps_cache_finalize;
+
+ g_object_class_install_property (object_class, PROP_PATH,
+ g_param_spec_string ("path", "Path", "The path to the cache", NULL,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static gchar *
+get_path (void)
+{
+ gchar *free_dir = NULL, *ret;
+ const gchar *dir, *path;
+
+ /* This should still work if it's the magic value ":memory:". */
+ path = g_getenv ("GABBLE_CAPS_CACHE");
+
+ if (path != NULL)
+ {
+ dir = free_dir = g_path_get_dirname (path);
+ ret = g_strdup (path);
+ }
+ else
+ {
+ dir = g_getenv ("GABBLE_CACHE_DIR");
+
+ if (dir != NULL)
+ {
+ ret = g_build_path (G_DIR_SEPARATOR_S, dir, "caps-cache.db", NULL);
+ }
+ else
+ {
+ ret = g_build_path (G_DIR_SEPARATOR_S,
+ g_get_user_cache_dir (), "telepathy", "gabble", "caps-cache.db",
+ NULL);
+ dir = free_dir = g_path_get_dirname (path);
+ }
+ }
+
+ /* Any errors are ignored here, on the basis that we'll find out the path is
+ * duff when we try to open the database anyway.
+ */
+ g_mkdir_with_parents (dir, 0755);
+ g_free (free_dir);
+ return ret;
+}
+
+static GObject *
+gabble_caps_cache_constructor (
+ GType type, guint n_props, GObjectConstructParam *props)
+{
+ int ret;
+ GabbleCapsCache *self;
+ gchar *error;
+
+ self = (GabbleCapsCache *) G_OBJECT_CLASS (gabble_caps_cache_parent_class)
+ ->constructor (type, n_props, props);
+
+ ret = sqlite3_open (self->priv->path, &self->priv->db);
+
+ if (ret == SQLITE_OK)
+ {
+ DEBUG ("opened database at %s", self->priv->path);
+ }
+ else
+ {
+ DEBUG ("opening database failed: %s", sqlite3_errmsg (self->priv->db));
+
+ /* Can't open it. Nuke it and try again. */
+ sqlite3_close (self->priv->db);
+ ret = unlink (self->priv->path);
+
+ if (!ret)
+ {
+ DEBUG ("removing database failed: %s", strerror (ret));
+
+ /* Can't open it or remove it. Just pretend it isn't there. */
+ self->priv->db = NULL;
+ }
+ else
+ {
+ ret = sqlite3_open (self->priv->path, &self->priv->db);
+
+ if (ret == SQLITE_OK)
+ {
+ DEBUG ("opened database at %s", self->priv->path);
+ }
+ else
+ {
+ DEBUG ("database open after remove failed: %s",
+ sqlite3_errmsg (self->priv->db));
+ /* Can't open it after removing it. Just pretend it isn't there.
+ */
+
+ sqlite3_close (self->priv->db);
+ self->priv->db = NULL;
+ }
+ }
+ }
+
+ ret = sqlite3_exec (self->priv->db,
+ "CREATE TABLE IF NOT EXISTS capabilities (\n"
+ " node text PRIMARY KEY,\n"
+ " namespaces text,\n"
+ " timestamp int)", NULL, NULL, &error);
+
+ if (ret != SQLITE_OK)
+ {
+ DEBUG ("failed to ensure table exists: %s", error);
+ sqlite3_free (error);
+ sqlite3_close (self->priv->db);
+ self->priv->db = NULL;
+ }
+
+ return (GObject *) self;
}
static void
@@ -65,15 +223,12 @@ gabble_caps_cache_init (GabbleCapsCache *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (
self, GABBLE_TYPE_CAPS_CACHE, GabbleCapsCachePrivate);
-
- self->priv->cache = g_hash_table_new_full (
- g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_strfreev);
}
GabbleCapsCache *
-gabble_caps_cache_new (void)
+gabble_caps_cache_new (const gchar *path)
{
- return g_object_new (GABBLE_TYPE_CAPS_CACHE, NULL);
+ return g_object_new (GABBLE_TYPE_CAPS_CACHE, "path", path, NULL);
}
GabbleCapsCache *
@@ -81,7 +236,11 @@ gabble_caps_cache_dup_shared (void)
{
if (shared_cache == NULL)
{
- shared_cache = gabble_caps_cache_new ();
+ gchar *path;
+
+ path = get_path ();
+ shared_cache = gabble_caps_cache_new (path);
+ g_free (path);
}
g_object_ref (shared_cache);
@@ -94,32 +253,322 @@ gabble_caps_cache_free_shared (void)
if (shared_cache != NULL)
{
g_object_unref (shared_cache);
+ shared_cache = NULL;
+ }
+}
+
+static gboolean
+caps_cache_prepare (
+ GabbleCapsCache *self,
+ const gchar *sql,
+ sqlite3_stmt **stmt)
+{
+ gint ret = sqlite3_prepare_v2 (self->priv->db, sql, -1, stmt, NULL);
+
+ if (ret != SQLITE_OK)
+ {
+ DEBUG ("preparing statement failed: %s",
+ sqlite3_errmsg (self->priv->db));
+ return FALSE;
+ }
+
+ g_assert (stmt != NULL);
+ return TRUE;
+}
+
+/* Finalizes @stmt if an error happens. */
+static gboolean
+caps_cache_bind_int (
+ GabbleCapsCache *self,
+ sqlite3_stmt *stmt,
+ gint param,
+ gint value)
+{
+ gint ret = sqlite3_bind_int (stmt, param, value);
+
+ if (ret != SQLITE_OK)
+ {
+ DEBUG ("parameter binding failed: %s", sqlite3_errmsg (self->priv->db));
+ sqlite3_finalize (stmt);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/* Finalizes @stmt if an error happens.
+ *
+ * Note: the parameter is bound statically, so it mustn't be freed before the
+ * statment is finalized.
+ */
+static gboolean
+caps_cache_bind_text (
+ GabbleCapsCache *self,
+ sqlite3_stmt *stmt,
+ gint param,
+ gint len,
+ const gchar *value)
+{
+ gint ret = sqlite3_bind_text (stmt, param, value, len, SQLITE_STATIC);
+
+ if (ret != SQLITE_OK)
+ {
+ DEBUG ("parameter binding failed: %s", sqlite3_errmsg (self->priv->db));
+ sqlite3_finalize (stmt);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/* Update cache entry timestmp. */
+static void
+caps_cache_touch (GabbleCapsCache *self, const gchar *node)
+{
+ gint ret;
+ sqlite3_stmt *stmt;
+
+ if (!caps_cache_prepare (self,
+ "UPDATE capabilities SET timestamp=? WHERE node=?", &stmt))
+ return;
+
+ if (!caps_cache_bind_int (self, stmt, 1, time (NULL)))
+ return;
+
+ if (!caps_cache_bind_text (self, stmt, 2, -1, node))
+ return;
+
+ ret = sqlite3_step (stmt);
+
+ if (ret != SQLITE_DONE)
+ {
+ DEBUG ("statement execution failed: %s",
+ sqlite3_errmsg (self->priv->db));
}
+
+ sqlite3_finalize (stmt);
}
+/* Caller is responsible for freeing the returned list and its contents.
+ */
gchar **
gabble_caps_cache_lookup (GabbleCapsCache *self, const gchar *node)
{
- return g_strdupv (g_hash_table_lookup (self->priv->cache, node));
+ gint ret;
+ const gchar *value = NULL;
+ sqlite3_stmt *stmt;
+ gchar **uris;
+
+ if (!self->priv->db)
+ /* DB open failed. */
+ return NULL;
+
+ if (!caps_cache_prepare (self,
+ "SELECT namespaces FROM capabilities WHERE node=?", &stmt))
+ return NULL;
+
+ if (!caps_cache_bind_text (self, stmt, 1, -1, node))
+ return NULL;
+
+ ret = sqlite3_step (stmt);
+
+ if (ret == SQLITE_DONE)
+ {
+ /* No result. */
+ DEBUG ("caps cache miss: %s", node);
+ sqlite3_finalize (stmt);
+ return NULL;
+ }
+
+ if (ret != SQLITE_ROW)
+ {
+ DEBUG ("statement execution failed: %s",
+ sqlite3_errmsg (self->priv->db));
+ sqlite3_finalize (stmt);
+ return NULL;
+ }
+
+ DEBUG ("caps cache hit: %s", node);
+ sqlite3_column_bytes (stmt, 0);
+ value = (gchar *) sqlite3_column_text (stmt, 0);
+ uris = g_strsplit (value, "\n", 0);
+ sqlite3_finalize (stmt);
+ caps_cache_touch (self, node);
+ return uris;
}
-void
-gabble_caps_cache_insert (
+static void
+caps_cache_insert (
GabbleCapsCache *self,
const gchar *node,
gchar **caps)
{
- GSList *old_caps;
+ gchar *val;
+ gint ret;
+ sqlite3_stmt *stmt;
+
+ if (!caps_cache_prepare (self,
+ "INSERT INTO capabilities (node, namespaces, timestamp) "
+ "VALUES (?, ?, ?)", &stmt))
+ return;
+
+ if (!caps_cache_bind_text (self, stmt, 1, -1, node))
+ return;
- old_caps = g_hash_table_lookup (self->priv->cache, node);
+ val = g_strjoinv ("\n", caps);
- if (old_caps != NULL)
+ if (!caps_cache_bind_text (self, stmt, 2, -1, val))
{
- /* XXX: issue warning here? */
+ g_free (val);
return;
}
- g_hash_table_insert (
- self->priv->cache, g_strdup (node), g_strdupv ((gchar **) caps));
+ if (!caps_cache_bind_int (self, stmt, 3, time (NULL)))
+ {
+ g_free (val);
+ return;
+ }
+
+ ret = sqlite3_step (stmt);
+
+ if (ret == SQLITE_CONSTRAINT)
+ {
+ /* Presumably the error is because the key already exists. Ignore it. */
+ sqlite3_finalize (stmt);
+ g_free (val);
+ return;
+ }
+
+ if (ret != SQLITE_DONE)
+ {
+ DEBUG ("statement execution failed: %s",
+ sqlite3_errmsg (self->priv->db));
+ sqlite3_finalize (stmt);
+ g_free (val);
+ return;
+ }
+
+ sqlite3_finalize (stmt);
+ g_free (val);
+}
+
+static gboolean
+caps_cache_count_entries (GabbleCapsCache *self, guint *count)
+{
+ gint ret;
+ sqlite3_stmt *stmt;
+
+ if (!self->priv->db)
+ return FALSE;
+
+ if (!caps_cache_prepare (self, "SELECT COUNT(*) FROM capabilities", &stmt))
+ return FALSE;
+
+ ret = sqlite3_step (stmt);
+
+ if (ret != SQLITE_ROW)
+ {
+ DEBUG ("statement execution failed: %s",
+ sqlite3_errmsg (self->priv->db));
+ sqlite3_finalize (stmt);
+ return FALSE;
+ }
+
+ *count = sqlite3_column_int (stmt, 0);
+ sqlite3_finalize (stmt);
+ return TRUE;
+}
+
+/* If the number of entries is above @high_threshold, remove entries older
+ * than @max_age while the cache is bigger than @low_threshold.
+ */
+static void
+caps_cache_gc (
+ GabbleCapsCache *self,
+ guint high_threshold,
+ guint low_threshold)
+{
+ gint ret;
+ guint count;
+ sqlite3_stmt *stmt;
+
+ if (!caps_cache_count_entries (self, &count))
+ return;
+
+ if (count <= high_threshold)
+ return;
+
+ /* This emulates DELETE ... ORDER ... LIMIT because some Sqlites (e.g.
+ * Debian) ship without SQLITE_ENABLE_UPDATE_DELETE_LIMIT unabled.
+ */
+
+ if (!caps_cache_prepare (self,
+ "DELETE FROM capabilities WHERE oid IN ("
+ " SELECT oid FROM capabilities"
+ " ORDER BY timestamp ASC, oid ASC"
+ " LIMIT ?)", &stmt))
+ return;
+
+ if (!caps_cache_bind_int (self, stmt, 1, count - low_threshold))
+ return;
+
+ ret = sqlite3_step (stmt);
+
+ if (ret != SQLITE_DONE)
+ {
+ DEBUG ("statement execution failed: %s",
+ sqlite3_errmsg (self->priv->db));
+ }
+
+ sqlite3_finalize (stmt);
+ DEBUG ("cache reduced from %d to %d items",
+ count, count - sqlite3_changes (self->priv->db));
+}
+
+static guint
+get_size (void)
+{
+ static gboolean ready = FALSE;
+ static guint size = 1000;
+
+ if (G_UNLIKELY (!ready))
+ {
+ const gchar *str = g_getenv ("GABBLE_CAPS_CACHE_SIZE");
+
+ if (str != NULL)
+ {
+ /* Ignoring return code; size will retain default value on failure.
+ */
+ sscanf (str, "%u", &size);
+ }
+
+ ready = TRUE;
+ /* DEBUG ("caps cache size = %d", size); */
+ }
+
+ return size;
+}
+
+void
+gabble_caps_cache_insert (
+ GabbleCapsCache *self,
+ const gchar *node,
+ gchar **caps)
+{
+ guint size = get_size ();
+
+ if (!self->priv->db)
+ /* DB open failed. */
+ return;
+
+ DEBUG ("caps cache insert: %s", node);
+ caps_cache_insert (self, node, caps);
+
+ /* Remove old entries after every 50th insert. */
+
+ if (self->priv->inserts % 50 == 0)
+ caps_cache_gc (self, size, MAX (1, 0.95 * size));
+
+ self->priv->inserts++;
}
diff --git a/src/caps-cache.h b/src/caps-cache.h
index 2afa6f6..d02e2bf 100644
--- a/src/caps-cache.h
+++ b/src/caps-cache.h
@@ -57,7 +57,7 @@ gabble_caps_cache_insert (
gchar **caps);
GabbleCapsCache *
-gabble_caps_cache_new (void);
+gabble_caps_cache_new (const gchar *path);
GabbleCapsCache *
gabble_caps_cache_dup_shared (void);
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am
index 4bb8503..8aab13b 100644
--- a/tests/twisted/Makefile.am
+++ b/tests/twisted/Makefile.am
@@ -5,6 +5,7 @@ TWISTED_TESTS = \
caps/advertise-draft1.py \
caps/advertise-legacy.py \
caps/caps-cache.py \
+ caps/caps-persistent-cache.py \
caps/compat-bundles.py \
caps/double-disco.py \
caps/from-bare-jid.py \
diff --git a/tests/twisted/caps/caps-persistent-cache.py b/tests/twisted/caps/caps-persistent-cache.py
new file mode 100644
index 0000000..4750330
--- /dev/null
+++ b/tests/twisted/caps/caps-persistent-cache.py
@@ -0,0 +1,81 @@
+
+from twisted.words.xish import xpath
+
+from servicetest import (
+ assertEquals, assertContains, assertDoesNotContain, EventPattern,
+ )
+from gabbletest import make_presence, exec_test
+from caps_helper import compute_caps_hash, send_disco_reply
+import constants as cs
+import ns
+
+contact_bare_jid = 'macbeth at glamis'
+contact_jid = 'macbeth at glamis/hall'
+client = 'http://telepathy.freedesktop.org/zomg-ponies'
+features = [
+ ns.JINGLE_015,
+ ns.JINGLE_015_AUDIO,
+ ns.JINGLE_015_VIDEO,
+ ns.GOOGLE_P2P,
+ ]
+
+def connect(q, conn):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+def send_presence(q, stream, contact_jid, identity):
+ ver = compute_caps_hash([identity], features, {})
+ stream.send(make_presence(contact_jid, status='Hello',
+ caps={'node': client, 'hash': 'sha-1', 'ver': ver}))
+
+def handle_disco(q, stream, contact_jid, identity):
+ # Gabble tries to resolve a caps hash.
+ ver = compute_caps_hash([identity], features, {})
+ event = q.expect('stream-iq', to=contact_jid, query_ns=ns.DISCO_INFO)
+ assertEquals(client + '#' + ver, event.query.attributes['node'])
+
+ # The bare jid replies.
+ send_disco_reply(stream, event.stanza, [identity], features)
+
+def capabilities_changed(q, contact_handle):
+ streamed_media_caps = (contact_handle, cs.CHANNEL_TYPE_STREAMED_MEDIA,
+ 0, 3, 0, cs.MEDIA_CAP_AUDIO | cs.MEDIA_CAP_VIDEO)
+ e = q.expect('dbus-signal', signal='CapabilitiesChanged')
+ assertContains(streamed_media_caps, e.args[0])
+
+def test1(q, bus, conn, stream):
+ connect(q, conn)
+ contact_handle = conn.RequestHandles(cs.HT_CONTACT, [contact_bare_jid])[0]
+ send_presence(q, stream, contact_jid, 'client/pc//thane')
+ handle_disco(q, stream, contact_jid, 'client/pc//thane')
+ capabilities_changed(q, contact_handle)
+
+def test2(q, bus, conn, stream):
+ # The second time around, the capabilities are retrieved from the cache,
+ # so no disco request is sent.
+ connect(q, conn)
+ contact_handle = conn.RequestHandles(cs.HT_CONTACT, [contact_bare_jid])[0]
+ send_presence(q, stream, contact_jid, 'client/pc//thane')
+ capabilities_changed(q, contact_handle)
+
+ # Overflow the cache. 51 is the cache size (during test runs) plus one.
+
+ for i in range(51):
+ overflow_contact_jid = 'witch%d at forest/cauldron' % i
+ overflow_identity = 'client/pc//prophecy%d' % i
+ send_presence(q, stream, overflow_contact_jid, overflow_identity)
+ handle_disco(q, stream, overflow_contact_jid, overflow_identity)
+
+if __name__ == '__main__':
+ # We run test1. The capabilities for macbeth at glamis's client
+ # need to be fetched via disco and are then stored in the cache.
+ exec_test(test1)
+ # We run test2 again. The capabilities are retrieved from the cache, so no
+ # disco request is sent. Then, a bunch of other clients turn up and force
+ # the entry for Macbeth's client out of the cache.
+ exec_test(test2)
+ # We run test1 again. The caps are no longer in the cache, so a disco
+ # request is sent again.
+ exec_test(test1)
+
diff --git a/tests/twisted/tools/exec-with-log.sh.in b/tests/twisted/tools/exec-with-log.sh.in
index ad24118..543a66e 100644
--- a/tests/twisted/tools/exec-with-log.sh.in
+++ b/tests/twisted/tools/exec-with-log.sh.in
@@ -3,6 +3,7 @@
cd "@abs_top_builddir@/tests/twisted/tools"
export GABBLE_DEBUG=all LM_DEBUG=net GIBBER_DEBUG=all
+export GABBLE_CAPS_CACHE=:memory: GABBLE_CAPS_CACHE_SIZE=50
ulimit -c unlimited
exec >> gabble-testing.log 2>&1
--
1.5.6.5
More information about the telepathy-commits
mailing list