diff options
Diffstat (limited to 'mate-dictionary/libgdict/gdict-client-context.c')
-rw-r--r-- | mate-dictionary/libgdict/gdict-client-context.c | 2162 |
1 files changed, 2162 insertions, 0 deletions
diff --git a/mate-dictionary/libgdict/gdict-client-context.c b/mate-dictionary/libgdict/gdict-client-context.c new file mode 100644 index 00000000..5e370f0a --- /dev/null +++ b/mate-dictionary/libgdict/gdict-client-context.c @@ -0,0 +1,2162 @@ +/* gdict-client-context.c - + * + * Copyright (C) 2005 Emmanuele Bassi <[email protected]> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + */ + +/** + * SECTION:gdict-client-context + * @short_description: DICT client transport + * + * #GdictClientContext is an implementation of the #GdictContext interface. + * It implements the Dictionary Protocol as defined by the RFC 2229 in order + * to connect to a dictionary server. + * + * You should rarely instantiate this object directely: use an appropriate + * #GdictSource instead. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <netdb.h> +#include <fcntl.h> +#include <errno.h> +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include "gdict-context-private.h" +#include "gdict-context.h" +#include "gdict-client-context.h" +#include "gdict-enum-types.h" +#include "gdict-marshal.h" +#include "gdict-debug.h" +#include "gdict-utils.h" +#include "gdict-private.h" + +typedef enum { + CMD_CLIENT, + CMD_SHOW_DB, + CMD_SHOW_STRAT, + CMD_SHOW_INFO, /* not implemented */ + CMD_SHOW_SERVER, /* not implemented */ + CMD_MATCH, + CMD_DEFINE, + CMD_STATUS, /* not implemented */ + CMD_OPTION_MIME, /* not implemented */ + CMD_AUTH, /* not implemented */ + CMD_HELP, /* not implemented */ + CMD_QUIT, + + CMD_INVALID +} GdictCommandType; +#define IS_VALID_CMD(cmd) (((cmd) >= CMD_CLIENT) || ((cmd) < CMD_INVALID)) + +/* command strings: keep synced with the enum above! */ +static const gchar *dict_command_strings[] = { + "CLIENT", + "SHOW DB", + "SHOW STRAT", + "SHOW INFO", + "SHOW SERVER", + "MATCH", + "DEFINE", + "STATUS", + "OPTION MIME", + "AUTH", + "HELP", + "QUIT", + + NULL +}; + +/* command stata */ +enum +{ + S_START, + + S_STATUS, + S_DATA, + + S_FINISH +}; + +typedef struct +{ + GdictCommandType cmd_type; + + gchar *cmd_string; + guint state; + + /* optional parameters passed to the command */ + gchar *database; + gchar *strategy; + gchar *word; + + /* buffer used to hold the reply from the server */ + GString *buffer; + + gpointer data; + GDestroyNotify data_destroy; +} GdictCommand; + +/* The default string to be passed to the CLIENT command */ +#define GDICT_DEFAULT_CLIENT "MATE Dictionary (" VERSION ")" + +/* Default server:port couple */ +#define GDICT_DEFAULT_HOSTNAME "dict.org" +#define GDICT_DEFAULT_PORT 2628 + +/* make the hostname lookup expire every five minutes */ +#define HOSTNAME_LOOKUP_EXPIRE 300 + +/* wait 30 seconds between connection and receiving data on the line */ +#define CONNECTION_TIMEOUT 30 + +enum +{ + PROP_0, + + PROP_HOSTNAME, + PROP_PORT, + PROP_STATUS, + PROP_CLIENT_NAME +}; + +enum +{ + CONNECTED, + DISCONNECTED, + + LAST_SIGNAL +}; + +static guint gdict_client_context_signals[LAST_SIGNAL] = { 0 }; + +#define GDICT_CLIENT_CONTEXT_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GDICT_TYPE_CLIENT_CONTEXT, GdictClientContextPrivate)) +struct _GdictClientContextPrivate +{ +#ifdef ENABLE_IPV6 + struct sockaddr_storage sockaddr; + struct addrinfo *host6info; +#else + struct sockaddr_in sockaddr; +#endif + struct hostent *hostinfo; + + time_t last_lookup; + + gchar *hostname; + gint port; + + GIOChannel *channel; + guint source_id; + guint timeout_id; + + GdictCommand *command; + GQueue *commands_queue; + + gchar *client_name; + + GdictStatusCode status_code; + + guint local_only : 1; + guint is_connecting : 1; +}; + +static void gdict_client_context_iface_init (GdictContextIface *iface); + +G_DEFINE_TYPE_WITH_CODE (GdictClientContext, + gdict_client_context, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GDICT_TYPE_CONTEXT, + gdict_client_context_iface_init)); + +/* GObject methods */ +static void gdict_client_context_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec); +static void gdict_client_context_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec); +static void gdict_client_context_finalize (GObject *object); + +/* GdictContext methods */ +static gboolean gdict_client_context_get_databases (GdictContext *context, + GError **error); +static gboolean gdict_client_context_get_strategies (GdictContext *context, + GError **error); +static gboolean gdict_client_context_define_word (GdictContext *context, + const gchar *database, + const gchar *word, + GError **error); +static gboolean gdict_client_context_match_word (GdictContext *context, + const gchar *database, + const gchar *strategy, + const gchar *word, + GError **error); + +static void gdict_client_context_clear_hostinfo (GdictClientContext *context); +static gboolean gdict_client_context_lookup_server (GdictClientContext *context, + GError **error); +static gboolean gdict_client_context_io_watch_cb (GIOChannel *source, + GIOCondition condition, + GdictClientContext *context); +static gboolean gdict_client_context_parse_line (GdictClientContext *context, + const gchar *buffer); +static void gdict_client_context_disconnect (GdictClientContext *context); +static void gdict_client_context_force_disconnect (GdictClientContext *context); +static void gdict_client_context_real_connected (GdictClientContext *context); +static void gdict_client_context_real_disconnected (GdictClientContext *context); + +static GdictCommand *gdict_command_new (GdictCommandType cmd_type); +static void gdict_command_free (GdictCommand *cmd); + + + +GQuark +gdict_client_context_error_quark (void) +{ + return g_quark_from_static_string ("gdict-client-context-error-quark"); +} + +static void +gdict_client_context_iface_init (GdictContextIface *iface) +{ + iface->get_databases = gdict_client_context_get_databases; + iface->get_strategies = gdict_client_context_get_strategies; + iface->match_word = gdict_client_context_match_word; + iface->define_word = gdict_client_context_define_word; +} + +static void +gdict_client_context_class_init (GdictClientContextClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->set_property = gdict_client_context_set_property; + gobject_class->get_property = gdict_client_context_get_property; + gobject_class->finalize = gdict_client_context_finalize; + + g_object_class_override_property (gobject_class, + GDICT_CONTEXT_PROP_LOCAL_ONLY, + "local-only"); + + /** + * GdictClientContext:client-name + * + * The name of the client using this context; it will be advertised when + * connecting to the dictionary server. + * + * Since: 1.0 + */ + g_object_class_install_property (gobject_class, + PROP_CLIENT_NAME, + g_param_spec_string ("client-name", + _("Client Name"), + _("The name of the client of the context object"), + NULL, + (G_PARAM_READABLE | G_PARAM_WRITABLE))); + /** + * GdictClientContext:hostname + * + * The hostname of the dictionary server to connect to. + * + * Since: 1.0 + */ + g_object_class_install_property (gobject_class, + PROP_HOSTNAME, + g_param_spec_string ("hostname", + _("Hostname"), + _("The hostname of the dictionary server to connect to"), + NULL, + (G_PARAM_READABLE | G_PARAM_WRITABLE))); + /** + * GdictClientContext:port + * + * The port of the dictionary server to connect to. + * + * Since: 1.0 + */ + g_object_class_install_property (gobject_class, + PROP_PORT, + g_param_spec_uint ("port", + _("Port"), + _("The port of the dictionary server to connect to"), + 0, + 65535, + GDICT_DEFAULT_PORT, + (G_PARAM_READABLE | G_PARAM_WRITABLE))); + /** + * GdictClientContext:status + * + * The status code as returned by the dictionary server. + * + * Since: 1.0 + */ + g_object_class_install_property (gobject_class, + PROP_STATUS, + g_param_spec_enum ("status", + _("Status"), + _("The status code as returned by the dictionary server"), + GDICT_TYPE_STATUS_CODE, + GDICT_STATUS_INVALID, + G_PARAM_READABLE)); + + /** + * GdictClientContext::connected + * @client: the object which received the signal + * + * Emitted when a #GdictClientContext has successfully established a + * connection with a dictionary server. + * + * Since: 1.0 + */ + gdict_client_context_signals[CONNECTED] = + g_signal_new ("connected", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GdictClientContextClass, connected), + NULL, NULL, + gdict_marshal_VOID__VOID, + G_TYPE_NONE, 0); + /** + * GdictClientContext::disconnected + * @client: the object which received the signal + * + * Emitted when a #GdictClientContext has disconnected from a dictionary + * server. + * + * Since: 1.0 + */ + gdict_client_context_signals[DISCONNECTED] = + g_signal_new ("disconnected", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GdictClientContextClass, disconnected), + NULL, NULL, + gdict_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + klass->connected = gdict_client_context_real_connected; + klass->disconnected = gdict_client_context_real_disconnected; + + g_type_class_add_private (gobject_class, sizeof (GdictClientContextPrivate)); +} + +static void +gdict_client_context_init (GdictClientContext *context) +{ + GdictClientContextPrivate *priv; + + priv = GDICT_CLIENT_CONTEXT_GET_PRIVATE (context); + context->priv = priv; + + priv->hostname = NULL; + priv->port = 0; + + priv->hostinfo = NULL; +#ifdef ENABLE_IPV6 + priv->host6info = NULL; +#endif + + priv->last_lookup = (time_t) -1; + + priv->is_connecting = FALSE; + priv->local_only = FALSE; + + priv->status_code = GDICT_STATUS_INVALID; + + priv->client_name = NULL; + + priv->command = NULL; + priv->commands_queue = g_queue_new (); +} + +static void +gdict_client_context_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GdictClientContextPrivate *priv = GDICT_CLIENT_CONTEXT_GET_PRIVATE (object); + + switch (prop_id) + { + case PROP_HOSTNAME: + if (priv->hostname) + g_free (priv->hostname); + priv->hostname = g_strdup (g_value_get_string (value)); + gdict_client_context_clear_hostinfo (GDICT_CLIENT_CONTEXT (object)); + break; + case PROP_PORT: + priv->port = g_value_get_uint (value); + break; + case PROP_CLIENT_NAME: + if (priv->client_name) + g_free (priv->client_name); + priv->client_name = g_strdup (g_value_get_string (value)); + break; + case GDICT_CONTEXT_PROP_LOCAL_ONLY: + priv->local_only = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gdict_client_context_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GdictClientContextPrivate *priv = GDICT_CLIENT_CONTEXT_GET_PRIVATE (object); + + switch (prop_id) + { + case PROP_STATUS: + g_value_set_enum (value, priv->status_code); + break; + case PROP_HOSTNAME: + g_value_set_string (value, priv->hostname); + break; + case PROP_PORT: + g_value_set_uint (value, priv->port); + break; + case PROP_CLIENT_NAME: + g_value_set_string (value, priv->client_name); + break; + case GDICT_CONTEXT_PROP_LOCAL_ONLY: + g_value_set_boolean (value, priv->local_only); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gdict_client_context_finalize (GObject *object) +{ + GdictClientContext *context = GDICT_CLIENT_CONTEXT (object); + GdictClientContextPrivate *priv = context->priv; + + /* force disconnection */ + gdict_client_context_force_disconnect (context); + + if (priv->command) + gdict_command_free (priv->command); + + if (priv->commands_queue) + { + g_queue_foreach (priv->commands_queue, + (GFunc) gdict_command_free, + NULL); + g_queue_free (priv->commands_queue); + + priv->commands_queue = NULL; + } + + if (priv->client_name) + g_free (priv->client_name); + + if (priv->hostname) + g_free (priv->hostname); + +#ifdef ENABLE_IPV6 + if (priv->host6info) + freeaddrinfo (priv->host6info); +#endif + + /* chain up parent's finalize method */ + G_OBJECT_CLASS (gdict_client_context_parent_class)->finalize (object); +} + +/** + * gdict_client_context_new: + * @hostname: the hostname of a dictionary server, or %NULL for the + * default server + * @port: port to be used when connecting to the dictionary server, + * or -1 for the default port + * + * Creates a new #GdictClientContext object for @hostname. Use this + * object to connect and query the dictionary server using the Dictionary + * Protocol as defined by RFC 2229. + * + * Return value: the newly created #GdictClientContext object. You should + * free it using g_object_unref(). + */ +GdictContext * +gdict_client_context_new (const gchar *hostname, + gint port) +{ + return g_object_new (GDICT_TYPE_CLIENT_CONTEXT, + "hostname", (hostname != NULL ? hostname : GDICT_DEFAULT_HOSTNAME), + "port", (port != -1 ? port : GDICT_DEFAULT_PORT), + "client-name", GDICT_DEFAULT_CLIENT, + NULL); +} + +/** + * gdict_client_context_set_hostname: + * @context: a #GdictClientContext + * @hostname: the hostname of a Dictionary server, or %NULL + * + * Sets @hostname as the hostname of the dictionary server to be used. + * If @hostname is %NULL, the default dictionary server will be used. + */ +void +gdict_client_context_set_hostname (GdictClientContext *context, + const gchar *hostname) +{ + g_return_if_fail (GDICT_IS_CLIENT_CONTEXT (context)); + + g_object_set (G_OBJECT (context), + "hostname", (hostname != NULL ? hostname : GDICT_DEFAULT_HOSTNAME), + NULL); +} + +/** + * gdict_client_context_get_hostname: + * @context: a #GdictClientContext + * + * Gets the hostname of the dictionary server used by @context. + * + * Return value: the hostname of a dictionary server. The returned string is + * owned by the #GdictClientContext object and should never be modified or + * freed. + */ +const gchar * +gdict_client_context_get_hostname (GdictClientContext *context) +{ + gchar *hostname; + + g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), NULL); + + g_object_get (G_OBJECT (context), "hostname", &hostname, NULL); + + return hostname; +} + +/** + * gdict_client_context_set_port: + * @context: a #GdictClientContext + * @port: port of the dictionary server to be used, or -1 + * + * Sets the port of the dictionary server to be used when connecting. + * + * If @port is -1, the default port will be used. + */ +void +gdict_client_context_set_port (GdictClientContext *context, + gint port) +{ + g_return_if_fail (GDICT_IS_CLIENT_CONTEXT (context)); + + g_object_set (G_OBJECT (context), + "port", (port != -1 ? port : GDICT_DEFAULT_PORT), + NULL); +} + +/** + * gdict_client_context_get_port: + * @context: a #GdictClientContext + * + * Gets the port of the dictionary server used by @context. + * + * Return value: the number of the port. + */ +guint +gdict_client_context_get_port (GdictClientContext *context) +{ + guint port; + + g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), -1); + + g_object_get (G_OBJECT (context), "port", &port, NULL); + + return port; +} + +/** + * gdict_client_context_set_client: + * @context: a #GdictClientContext + * @client: the client name to use, or %NULL + * + * Sets @client as the client name to be used when advertising ourselves when + * a connection the the dictionary server has been established. + * If @client is %NULL, the default client name will be used. + */ +void +gdict_client_context_set_client (GdictClientContext *context, + const gchar *client) +{ + g_return_if_fail (GDICT_IS_CLIENT_CONTEXT (context)); + + g_object_set (G_OBJECT (context), + "client-name", (client != NULL ? client : GDICT_DEFAULT_CLIENT), + NULL); +} + +/** + * gdict_client_context_get_client: + * @context: a #GdictClientContext + * + * Gets the client name used by @context. See gdict_client_context_set_client(). + * + * Return value: the client name. The returned string is owned by the + * #GdictClientContext object and should never be modified or freed. + */ +const gchar * +gdict_client_context_get_client (GdictClientContext *context) +{ + gchar *client_name; + + g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), NULL); + + g_object_get (G_OBJECT (context), "client-name", &client_name, NULL); + + return client_name; +} + +/* creates a new command to be sent to the dictionary server */ +static GdictCommand * +gdict_command_new (GdictCommandType cmd_type) +{ + GdictCommand *retval; + + g_assert (IS_VALID_CMD (cmd_type)); + + retval = g_slice_new0 (GdictCommand); + + retval->cmd_type = cmd_type; + retval->state = S_START; + + return retval; +} + +static void +gdict_command_free (GdictCommand *cmd) +{ + if (!cmd) + return; + + g_free (cmd->cmd_string); + + switch (cmd->cmd_type) + { + case CMD_CLIENT: + case CMD_QUIT: + break; + case CMD_SHOW_DB: + case CMD_SHOW_STRAT: + break; + case CMD_MATCH: + g_free (cmd->database); + g_free (cmd->strategy); + g_free (cmd->word); + break; + case CMD_DEFINE: + g_free (cmd->database); + g_free (cmd->word); + break; + default: + break; + } + + if (cmd->buffer) + g_string_free (cmd->buffer, TRUE); + + if (cmd->data_destroy) + cmd->data_destroy (cmd->data); + + g_slice_free (GdictCommand, cmd); +} + +/* push @command into the head of the commands queue; the command queue is + * a FIFO-like pipe: commands go into the head and are retrieved from the + * tail. + */ +static gboolean +gdict_client_context_push_command (GdictClientContext *context, + GdictCommand *command) +{ + GdictClientContextPrivate *priv; + + g_assert (GDICT_IS_CLIENT_CONTEXT (context)); + g_assert (command != NULL); + + priv = context->priv; + + /* avoid pushing a command twice */ + if (g_queue_find (priv->commands_queue, command)) + { + g_warning ("gdict_client_context_push_command() called on a command already in queue\n"); + return FALSE; + } + + GDICT_NOTE (DICT, "Pushing command ('%s') into the queue...", + dict_command_strings[command->cmd_type]); + + g_queue_push_head (priv->commands_queue, command); + + return TRUE; +} + +static GdictCommand * +gdict_client_context_pop_command (GdictClientContext *context) +{ + GdictClientContextPrivate *priv; + GdictCommand *retval; + + g_assert (GDICT_IS_CLIENT_CONTEXT (context)); + + priv = context->priv; + + retval = (GdictCommand *) g_queue_pop_tail (priv->commands_queue); + if (!retval) + return NULL; + + GDICT_NOTE (DICT, "Getting command ('%s') from the queue...", + dict_command_strings[retval->cmd_type]); + + return retval; +} + +/* send @command on the wire */ +static gboolean +gdict_client_context_send_command (GdictClientContext *context, + GdictCommand *command, + GError **error) +{ + GdictClientContextPrivate *priv; + GError *write_error; + gsize written_bytes; + GIOStatus res; + + g_assert (GDICT_IS_CLIENT_CONTEXT (context)); + g_assert (command != NULL && command->cmd_string != NULL); + + priv = context->priv; + + if (!priv->channel) + { + GDICT_NOTE (DICT, "No connection established"); + + g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_NO_CONNECTION, + _("No connection to the dictionary server at '%s:%d'"), + priv->hostname, + priv->port); + + return FALSE; + } + + write_error = NULL; + res = g_io_channel_write_chars (priv->channel, + command->cmd_string, + -1, + &written_bytes, + &write_error); + if (res != G_IO_STATUS_NORMAL) + { + g_propagate_error (error, write_error); + + return FALSE; + } + + /* force flushing of the write buffer */ + g_io_channel_flush (priv->channel, NULL); + + GDICT_NOTE (DICT, "Wrote %"G_GSIZE_FORMAT" bytes to the channel", written_bytes); + + return TRUE; +} + +/* gdict_client_context_run_command: runs @command inside @context; this + * function builds the command string and then passes it to the dictionary + * server. + */ +static gboolean +gdict_client_context_run_command (GdictClientContext *context, + GdictCommand *command, + GError **error) +{ + GdictClientContextPrivate *priv; + gchar *payload; + GError *send_error; + gboolean res; + + g_assert (GDICT_IS_CLIENT_CONTEXT (context)); + g_assert (command != NULL); + g_assert (IS_VALID_CMD (command->cmd_type)); + + GDICT_NOTE (DICT, "GdictCommand command =\n" + "{\n" + " .cmd_type = '%02d' ('%s');\n" + " .database = '%s';\n" + " .strategy = '%s';\n" + " .word = '%s';\n" + "}\n", + command->cmd_type, dict_command_strings[command->cmd_type], + command->database ? command->database : "<none>", + command->strategy ? command->strategy : "<none>", + command->word ? command->word : "<none>"); + + priv = context->priv; + + g_assert (priv->command == NULL); + + priv->command = command; + + /* build the command string to be sent to the server */ + switch (command->cmd_type) + { + case CMD_CLIENT: + payload = g_shell_quote (priv->client_name); + command->cmd_string = g_strdup_printf ("%s %s\r\n", + dict_command_strings[CMD_CLIENT], + payload); + g_free (payload); + break; + case CMD_QUIT: + command->cmd_string = g_strdup_printf ("%s\r\n", + dict_command_strings[CMD_QUIT]); + break; + case CMD_SHOW_DB: + command->cmd_string = g_strdup_printf ("%s\r\n", + dict_command_strings[CMD_SHOW_DB]); + break; + case CMD_SHOW_STRAT: + command->cmd_string = g_strdup_printf ("%s\r\n", + dict_command_strings[CMD_SHOW_STRAT]); + break; + case CMD_MATCH: + g_assert (command->word); + payload = g_shell_quote (command->word); + command->cmd_string = g_strdup_printf ("%s %s %s %s\r\n", + dict_command_strings[CMD_MATCH], + (command->database != NULL ? command->database : "!"), + (command->strategy != NULL ? command->strategy : "*"), + payload); + g_free (payload); + break; + case CMD_DEFINE: + g_assert (command->word); + payload = g_shell_quote (command->word); + command->cmd_string = g_strdup_printf ("%s %s %s\r\n", + dict_command_strings[CMD_DEFINE], + (command->database != NULL ? command->database : "!"), + payload); + g_free (payload); + break; + default: + g_assert_not_reached (); + break; + } + + g_assert (command->cmd_string); + + GDICT_NOTE (DICT, "Sending command ('%s') to the server", + dict_command_strings[command->cmd_type]); + + send_error = NULL; + res = gdict_client_context_send_command (context, command, &send_error); + if (!res) + { + g_propagate_error (error, send_error); + + return FALSE; + } + + return TRUE; +} + +/* we use this signal to advertise ourselves to the dictionary server */ +static void +gdict_client_context_real_connected (GdictClientContext *context) +{ + GdictCommand *cmd; + + cmd = gdict_command_new (CMD_CLIENT); + cmd->state = S_FINISH; + + /* the CLIENT command should be the first one in our queue, so we place + * it above all other commands the user might have issued between the + * first and the emission of the "connected" signal, by calling it + * directely. + */ + gdict_client_context_run_command (context, cmd, NULL); +} + +static void +clear_command_queue (GdictClientContext *context) +{ + GdictClientContextPrivate *priv = context->priv; + + if (priv->commands_queue) + { + g_queue_foreach (priv->commands_queue, + (GFunc) gdict_command_free, + NULL); + + g_queue_free (priv->commands_queue); + } + + /* renew */ + priv->commands_queue = g_queue_new (); +} + +/* force a disconnection from the server */ +static void +gdict_client_context_force_disconnect (GdictClientContext *context) +{ + GdictClientContextPrivate *priv = context->priv; + + if (priv->timeout_id) + { + g_source_remove (priv->timeout_id); + priv->timeout_id = 0; + } + + if (priv->source_id) + { + g_source_remove (priv->source_id); + priv->source_id = 0; + } + + if (priv->channel) + { + g_io_channel_shutdown (priv->channel, TRUE, NULL); + g_io_channel_unref (priv->channel); + + priv->channel = NULL; + } + + if (priv->command) + { + gdict_command_free (priv->command); + priv->command = NULL; + } + + clear_command_queue (context); +} + +static void +gdict_client_context_real_disconnected (GdictClientContext *context) +{ + gdict_client_context_force_disconnect (context); +} + +/* clear the lookup data */ +static void +gdict_client_context_clear_hostinfo (GdictClientContext *context) +{ + GdictClientContextPrivate *priv; + + g_assert (GDICT_IS_CLIENT_CONTEXT (context)); + + priv = context->priv; + +#ifdef ENABLE_IPV6 + if (!priv->host6info) + return; +#endif + + if (!priv->hostinfo) + return; + +#ifdef ENABLE_IPV6 + freeaddrinfo (priv->host6info); +#endif + priv->hostinfo = NULL; +} + +/* gdict_client_context_lookup_server: perform an hostname lookup in order to + * connect to the dictionary server + */ +static gboolean +gdict_client_context_lookup_server (GdictClientContext *context, + GError **error) +{ + GdictClientContextPrivate *priv; + gboolean is_expired = FALSE; + time_t now; + + g_assert (GDICT_IS_CLIENT_CONTEXT (context)); + + priv = context->priv; + + /* we need the hostname, at this point */ + g_assert (priv->hostname != NULL); + + time (&now); + if (now >= (priv->last_lookup + HOSTNAME_LOOKUP_EXPIRE)) + is_expired = TRUE; + + /* we already have resolved the hostname */ +#ifdef ENABLE_IPV6 + if (priv->host6info && !is_expired) + return TRUE; +#endif + + if (priv->hostinfo && !is_expired) + return TRUE; + + /* clear any previously acquired lookup data */ + gdict_client_context_clear_hostinfo (context); + + GDICT_NOTE (DICT, "Looking up hostname '%s'", priv->hostname); + +#ifdef ENABLE_IPV6 + if (_gdict_has_ipv6 ()) + { + struct addrinfo hints, *res; + + GDICT_NOTE (DICT, "Hostname '%s' look-up (using IPv6)", priv->hostname); + + memset (&hints, 0, sizeof (hints)); + hints.ai_socktype = SOCK_STREAM; + + if (getaddrinfo (priv->hostname, NULL, &hints, &(priv->host6info)) == 0) + { + for (res = priv->host6info; res; res = res->ai_next) + if (res->ai_family == AF_INET6 || res->ai_family == AF_INET) + break; + + if (!res) + { + g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_LOOKUP, + _("Lookup failed for hostname '%s': no suitable resources found"), + priv->hostname); + + return FALSE; + } + else + { + if (res->ai_family == AF_INET6) + memcpy (&((struct sockaddr_in6 *) &priv->sockaddr)->sin6_addr, + &((struct sockaddr_in6 *) res->ai_addr)->sin6_addr, + sizeof (struct in6_addr)); + + if (res->ai_family == AF_INET) + memcpy (&((struct sockaddr_in *) &priv->sockaddr)->sin_addr, + &((struct sockaddr_in *) res->ai_addr)->sin_addr, + sizeof (struct in_addr)); + + priv->sockaddr.ss_family = res->ai_family; + + GDICT_NOTE (DICT, "Hostname '%s' found (using IPv6)", + priv->hostname); + + priv->last_lookup = time (NULL); + + return TRUE; + } + } + else + { + g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_LOOKUP, + _("Lookup failed for host '%s': %s"), + priv->hostname, + gai_strerror (errno)); + + return FALSE; + } + } + else + { +#endif /* ENABLE_IPV6 */ + /* if we don't support IPv6, fallback to usual IPv4 lookup */ + + GDICT_NOTE (DICT, "Hostname '%s' look-up (using IPv4)", priv->hostname); + + ((struct sockaddr_in *) &priv->sockaddr)->sin_family = AF_INET; + + priv->hostinfo = gethostbyname (priv->hostname); + if (priv->hostinfo) + { + memcpy (&((struct sockaddr_in *) &(priv->sockaddr))->sin_addr, + priv->hostinfo->h_addr, + priv->hostinfo->h_length); + + GDICT_NOTE (DICT, "Hostname '%s' found (using IPv4)", + priv->hostname); + + priv->last_lookup = time (NULL); + + return TRUE; + } + else + { + g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_LOOKUP, + _("Lookup failed for host '%s': host not found"), + priv->hostname); + + return FALSE; + } +#ifdef ENABLE_IPV6 + } +#endif + + g_assert_not_reached (); + + return FALSE; +} + +/* gdict_client_context_parse_line: parses a line from the dictionary server + * this is the core of the RFC2229 protocol implementation, here's where + * the magic happens. + */ +static gboolean +gdict_client_context_parse_line (GdictClientContext *context, + const gchar *buffer) +{ + GdictClientContextPrivate *priv; + GError *server_error = NULL; + + g_assert (GDICT_IS_CLIENT_CONTEXT (context)); + g_assert (buffer != NULL); + + priv = context->priv; + + GDICT_NOTE (DICT, "parse buffer: '%s'", buffer); + + /* connection is a special case: we don't have a command, so we just + * make sure that the server replied with the correct code. WARNING: + * the server might be shutting down or not available, so we must + * take into account those responses too! + */ + if (!priv->command) + { + if (priv->status_code == GDICT_STATUS_CONNECT) + { + /* the server accepts our connection */ + g_signal_emit (context, gdict_client_context_signals[CONNECTED], 0); + + return TRUE; + } + else if ((priv->status_code == GDICT_STATUS_SERVER_DOWN) || + (priv->status_code == GDICT_STATUS_SHUTDOWN)) + { + /* the server is shutting down or is not available */ + g_set_error (&server_error, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_SERVER_DOWN, + _("Unable to connect to the dictionary server " + "at '%s:%d'. The server replied with " + "code %d (server down)"), + priv->hostname, + priv->port, + priv->status_code); + + g_signal_emit_by_name (context, "error", server_error); + + g_error_free (server_error); + + return TRUE; + } + else + { + GError *parse_error = NULL; + + g_set_error (&parse_error, GDICT_CONTEXT_ERROR, + GDICT_CONTEXT_ERROR_PARSE, + _("Unable to parse the dictionary server reply\n: '%s'"), + buffer); + + g_signal_emit_by_name (context, "error", parse_error); + + g_error_free (parse_error); + + return FALSE; + } + } + + /* disconnection is another special case: the server replies with code + * 221, and closes the connection; we emit the "disconnected" signal + * and close the connection on our side. + */ + if (priv->status_code == GDICT_STATUS_QUIT) + { + g_signal_emit (context, gdict_client_context_signals[DISCONNECTED], 0); + + return TRUE; + } + + /* here we catch all the errors codes that the server might give us */ + server_error = NULL; + switch (priv->status_code) + { + case GDICT_STATUS_NO_MATCH: + g_set_error (&server_error, GDICT_CONTEXT_ERROR, + GDICT_CONTEXT_ERROR_NO_MATCH, + _("No definitions found for '%s'"), + priv->command->word); + + GDICT_NOTE (DICT, "No match: %s", server_error->message); + + g_signal_emit_by_name (context, "error", server_error); + + g_error_free (server_error); + server_error = NULL; + + priv->command->state = S_FINISH; + break; + case GDICT_STATUS_BAD_DATABASE: + g_set_error (&server_error, GDICT_CONTEXT_ERROR, + GDICT_CONTEXT_ERROR_INVALID_DATABASE, + _("Invalid database '%s'"), + priv->command->database); + + GDICT_NOTE (DICT, "Bad DB: %s", server_error->message); + + g_signal_emit_by_name (context, "error", server_error); + + g_error_free (server_error); + server_error = NULL; + + priv->command->state = S_FINISH; + break; + case GDICT_STATUS_BAD_STRATEGY: + g_set_error (&server_error, GDICT_CONTEXT_ERROR, + GDICT_CONTEXT_ERROR_INVALID_STRATEGY, + _("Invalid strategy '%s'"), + priv->command->strategy); + + GDICT_NOTE (DICT, "Bad strategy: %s", server_error->message); + + g_signal_emit_by_name (context, "error", server_error); + + g_error_free (server_error); + server_error = NULL; + + priv->command->state = S_FINISH; + break; + case GDICT_STATUS_BAD_COMMAND: + g_set_error (&server_error, GDICT_CONTEXT_ERROR, + GDICT_CONTEXT_ERROR_INVALID_COMMAND, + _("Bad command '%s'"), + dict_command_strings[priv->command->cmd_type]); + + GDICT_NOTE (DICT, "Bad command: %s", server_error->message); + + g_signal_emit_by_name (context, "error", server_error); + + g_error_free (server_error); + server_error = NULL; + + priv->command->state = S_FINISH; + break; + case GDICT_STATUS_BAD_PARAMETERS: + g_set_error (&server_error, GDICT_CONTEXT_ERROR, + GDICT_CONTEXT_ERROR_INVALID_COMMAND, + _("Bad parameters for command '%s'"), + dict_command_strings[priv->command->cmd_type]); + + GDICT_NOTE (DICT, "Bad params: %s", server_error->message); + + g_signal_emit_by_name (context, "error", server_error); + + g_error_free (server_error); + server_error = NULL; + + priv->command->state = S_FINISH; + break; + case GDICT_STATUS_NO_DATABASES_PRESENT: + g_set_error (&server_error, GDICT_CONTEXT_ERROR, + GDICT_CONTEXT_ERROR_NO_DATABASES, + _("No databases found on dictionary server at '%s'"), + priv->hostname); + + GDICT_NOTE (DICT, "No DB: %s", server_error->message); + + g_signal_emit_by_name (context, "error", server_error); + + g_error_free (server_error); + server_error = NULL; + + priv->command->state = S_FINISH; + break; + case GDICT_STATUS_NO_STRATEGIES_PRESENT: + g_set_error (&server_error, GDICT_CONTEXT_ERROR, + GDICT_CONTEXT_ERROR_NO_STRATEGIES, + _("No strategies found on dictionary server at '%s'"), + priv->hostname); + + GDICT_NOTE (DICT, "No strategies: %s", server_error->message); + + g_signal_emit_by_name (context, "error", server_error); + + g_error_free (server_error); + server_error = NULL; + + priv->command->state = S_FINISH; + break; + default: + GDICT_NOTE (DICT, "non-error code: %d", priv->status_code); + break; + } + + /* server replied with 'ok' or the command has reached its FINISH state, + * so now we are clear for destroying the current command and check if + * there are other commands on the queue, and run them. + */ + if ((priv->status_code == GDICT_STATUS_OK) || + (priv->command->state == S_FINISH)) + { + GdictCommand *new_command; + GError *run_error; + GdictCommandType last_cmd; + + last_cmd = priv->command->cmd_type; + + gdict_command_free (priv->command); + priv->command = NULL; + + /* notify the end of a command - ignore CLIENT and QUIT commands, as + * we issue them ourselves + */ + if ((last_cmd != CMD_CLIENT) && (last_cmd != CMD_QUIT)) + g_signal_emit_by_name (context, "lookup-end"); + + /* pop the next command from the queue */ + new_command = gdict_client_context_pop_command (context); + if (!new_command) + { + /* if the queue is empty, quit */ + gdict_client_context_disconnect (context); + new_command = gdict_client_context_pop_command (context); + } + + run_error = NULL; + gdict_client_context_run_command (context, new_command, &run_error); + if (run_error) + { + g_signal_emit_by_name (context, "error", run_error); + + g_error_free (run_error); + } + + return TRUE; + } + + GDICT_NOTE (DICT, "check command %d ('%s')[state:%d]", + priv->command->cmd_type, + dict_command_strings[priv->command->cmd_type], + priv->command->state); + + /* check command type */ + switch (priv->command->cmd_type) + { + case CMD_CLIENT: + case CMD_QUIT: + break; + case CMD_SHOW_DB: + if (priv->status_code == GDICT_STATUS_N_DATABASES_PRESENT) + { + gchar *p; + + priv->command->state = S_DATA; + + p = g_utf8_strchr (buffer, -1, ' '); + if (p) + p = g_utf8_next_char (p); + + GDICT_NOTE (DICT, "server replied: %d databases found", atoi (p)); + + g_signal_emit_by_name (context, "lookup-start"); + } + else if (0 == strcmp (buffer, ".")) + priv->command->state = S_FINISH; + else + { + GdictDatabase *db; + gchar *name, *full, *p; + + g_assert (priv->command->state == S_DATA); + + /* first token: database name; + * second token: database description; + */ + name = (gchar *) buffer; + if (!name) + break; + + p = g_utf8_strchr (name, -1, ' '); + if (p) + *p = '\0'; + + full = g_utf8_next_char (p); + + if (full[0] == '\"') + full = g_utf8_next_char (full); + + p = g_utf8_strchr (full, -1, '\"'); + if (p) + *p = '\0'; + + db = _gdict_database_new (name); + db->full_name = g_strdup (full); + + g_signal_emit_by_name (context, "database-found", db); + + gdict_database_unref (db); + } + break; + case CMD_SHOW_STRAT: + if (priv->status_code == GDICT_STATUS_N_STRATEGIES_PRESENT) + { + gchar *p; + + priv->command->state = S_DATA; + + p = g_utf8_strchr (buffer, -1, ' '); + if (p) + p = g_utf8_next_char (p); + + GDICT_NOTE (DICT, "server replied: %d strategies found", atoi (p)); + + g_signal_emit_by_name (context, "lookup-start"); + } + else if (0 == strcmp (buffer, ".")) + priv->command->state = S_FINISH; + else + { + GdictStrategy *strat; + gchar *name, *desc, *p; + + g_assert (priv->command->state == S_DATA); + + name = (gchar *) buffer; + if (!name) + break; + + p = g_utf8_strchr (name, -1, ' '); + if (p) + *p = '\0'; + + desc = g_utf8_next_char (p); + + if (desc[0] == '\"') + desc = g_utf8_next_char (desc); + + p = g_utf8_strchr (desc, -1, '\"'); + if (p) + *p = '\0'; + + strat = _gdict_strategy_new (name); + strat->description = g_strdup (desc); + + g_signal_emit_by_name (context, "strategy-found", strat); + + gdict_strategy_unref (strat); + } + break; + case CMD_DEFINE: + if (priv->status_code == GDICT_STATUS_N_DEFINITIONS_RETRIEVED) + { + GdictDefinition *def; + gchar *p; + + priv->command->state = S_STATUS; + + p = g_utf8_strchr (buffer, -1, ' '); + if (p) + p = g_utf8_next_char (p); + + GDICT_NOTE (DICT, "server replied: %d definitions found", atoi (p)); + + def = _gdict_definition_new (atoi (p)); + + priv->command->data = def; + priv->command->data_destroy = (GDestroyNotify) gdict_definition_unref; + + g_signal_emit_by_name (context, "lookup-start"); + } + else if (priv->status_code == GDICT_STATUS_WORD_DB_NAME) + { + GdictDefinition *def; + gchar *word, *db_name, *db_full, *p; + + word = (gchar *) buffer; + + /* skip the status code */ + word = g_utf8_strchr (word, -1, ' '); + word = g_utf8_next_char (word); + + if (word[0] == '\"') + word = g_utf8_next_char (word); + + p = g_utf8_strchr (word, -1, '\"'); + if (p) + *p = '\0'; + + p = g_utf8_next_char (p); + + /* the database name is not protected by "" */ + db_name = g_utf8_next_char (p); + if (!db_name) + break; + + p = g_utf8_strchr (db_name, -1, ' '); + if (p) + *p = '\0'; + + p = g_utf8_next_char (p); + + db_full = g_utf8_next_char (p); + if (!db_full) + break; + + if (db_full[0] == '\"') + db_full = g_utf8_next_char (db_full); + + p = g_utf8_strchr (db_full, -1, '\"'); + if (p) + *p = '\0'; + + def = (GdictDefinition *) priv->command->data; + + GDICT_NOTE (DICT, "{ word = '%s', db_name = '%s', db_full = '%s' }", + word, + db_name, + db_full); + + def->word = g_strdup (word); + def->database_name = g_strdup (db_name); + def->database_full = g_strdup (db_full); + def->definition = NULL; + + priv->command->state = S_DATA; + } + else if (strcmp (buffer, ".") == 0) + { + GdictDefinition *def; + gint num; + + g_assert (priv->command->state == S_DATA); + + def = (GdictDefinition *) priv->command->data; + if (!def) + break; + + def->definition = g_string_free (priv->command->buffer, FALSE); + + /* store the numer of definitions */ + num = def->total; + + g_signal_emit_by_name (context, "definition-found", def); + + gdict_definition_unref (def); + + priv->command->buffer = NULL; + priv->command->data = _gdict_definition_new (num); + + priv->command->state = S_STATUS; + } + else + { + g_assert (priv->command->state == S_DATA); + + if (!priv->command->buffer) + priv->command->buffer = g_string_new (NULL); + + GDICT_NOTE (DICT, "appending to buffer:\n %s", buffer); + + /* TODO - collapse '..' to '.' */ + g_string_append_printf (priv->command->buffer, "%s\n", buffer); + } + break; + case CMD_MATCH: + if (priv->status_code == GDICT_STATUS_N_MATCHES_FOUND) + { + gchar *p; + + priv->command->state = S_DATA; + + p = g_utf8_strchr (buffer, -1, ' '); + if (p) + p = g_utf8_next_char (p); + + GDICT_NOTE (DICT, "server replied: %d matches found", atoi (p)); + + g_signal_emit_by_name (context, "lookup-start"); + } + else if (0 == strcmp (buffer, ".")) + priv->command->state = S_FINISH; + else + { + GdictMatch *match; + gchar *word, *db_name, *p; + + g_assert (priv->command->state == S_DATA); + + db_name = (gchar *) buffer; + if (!db_name) + break; + + p = g_utf8_strchr (db_name, -1, ' '); + if (p) + *p = '\0'; + + word = g_utf8_next_char (p); + + if (word[0] == '\"') + word = g_utf8_next_char (word); + + p = g_utf8_strchr (word, -1, '\"'); + if (p) + *p = '\0'; + + match = _gdict_match_new (word); + match->database = g_strdup (db_name); + + g_signal_emit_by_name (context, "match-found", match); + + gdict_match_unref (match); + } + break; + default: + g_assert_not_reached (); + break; + } + + return TRUE; +} + +/* retrieve the status code from the server response line */ +static gint +get_status_code (const gchar *line, + gint old_status) +{ + gchar *status; + gint possible_status, retval; + + if (strlen (line) < 3) + return 0; + + if (!g_unichar_isdigit (line[0]) || + !g_unichar_isdigit (line[1]) || + !g_unichar_isdigit (line[2])) + return 0; + + if (!g_unichar_isspace (line[3])) + return 0; + + status = g_strndup (line, 3); + possible_status = atoi (status); + g_free (status); + + /* status whitelisting: sometimes, a database *cough* moby-thes *cough* + * might return a number as first word; we do a small check here for + * invalid status codes based on the previously set status; we don't check + * the whole line, as we need only to be sure that the status code is + * consistent with what we expect. + */ + switch (old_status) + { + case GDICT_STATUS_WORD_DB_NAME: + case GDICT_STATUS_N_MATCHES_FOUND: + if (possible_status == GDICT_STATUS_OK) + retval = possible_status; + else + retval = 0; + break; + case GDICT_STATUS_N_DEFINITIONS_RETRIEVED: + if (possible_status == GDICT_STATUS_WORD_DB_NAME) + retval = possible_status; + else + retval = 0; + break; + default: + retval = possible_status; + break; + } + + return retval; +} + +static gboolean +gdict_client_context_io_watch_cb (GIOChannel *channel, + GIOCondition condition, + GdictClientContext *context) +{ + GdictClientContextPrivate *priv; + + g_assert (GDICT_IS_CLIENT_CONTEXT (context)); + priv = context->priv; + + /* since this is an asynchronous channel, we might end up here + * even though the channel has been shut down. + */ + if (!priv->channel) + { + g_warning ("No channel available\n"); + + return FALSE; + } + + if (priv->is_connecting) + { + priv->is_connecting = FALSE; + + if (priv->timeout_id) + { + g_source_remove (priv->timeout_id); + priv->timeout_id = 0; + } + } + + if (condition & G_IO_ERR) + { + GError *err = NULL; + + g_set_error (&err, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_SOCKET, + _("Connection failed to the dictionary server at %s:%d"), + priv->hostname, + priv->port); + + g_signal_emit_by_name (context, "error", err); + + g_error_free (err); + + return FALSE; + } + + while (1) + { + GIOStatus res; + guint status_code; + GError *read_err; + gsize len, term; + gchar *line; + gboolean parse_res; + + /* we might sever the connection while still inside the read loop, + * so we must check the state of the channel before actually doing + * the line reading, otherwise we'll end up with death, destruction + * and chaos on all earth. oh, and an assertion failed inside + * g_io_channel_read_line(). + */ + if (!priv->channel) + break; + + read_err = NULL; + res = g_io_channel_read_line (priv->channel, &line, &len, &term, &read_err); + if (res == G_IO_STATUS_ERROR) + { + if (read_err) + { + GError *err = NULL; + + g_set_error (&err, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_SOCKET, + _("Error while reading reply from server:\n%s"), + read_err->message); + + g_signal_emit_by_name (context, "error", err); + + g_error_free (err); + g_error_free (read_err); + } + + gdict_client_context_force_disconnect (context); + + return FALSE; + } + + if (len == 0) + break; + + /* truncate the line terminator before parsing */ + line[term] = '\0'; + + status_code = get_status_code (line, priv->status_code); + if ((status_code == 0) || (GDICT_IS_VALID_STATUS_CODE (status_code))) + { + priv->status_code = status_code; + + GDICT_NOTE (DICT, "new status = '%d'", priv->status_code); + } + else + priv->status_code = GDICT_STATUS_INVALID; + + /* notify changes only for valid status codes */ + if (priv->status_code != GDICT_STATUS_INVALID) + g_object_notify (G_OBJECT (context), "status"); + + parse_res = gdict_client_context_parse_line (context, line); + if (!parse_res) + { + g_free (line); + + g_warning ("Parsing failed"); + + gdict_client_context_force_disconnect (context); + + return FALSE; + } + + g_free (line); + } + + return TRUE; +} + +static gboolean +check_for_connection (gpointer data) +{ + GdictClientContext *context = data; + +#if 0 + g_debug (G_STRLOC ": checking for connection (is connecting:%s)", + context->priv->is_connecting ? "true" : "false"); +#endif + + if (context == NULL) + return FALSE; + + if (context->priv->is_connecting) + { + GError *err = NULL; + + GDICT_NOTE (DICT, "Forcing a disconnection due to timeout"); + + g_set_error (&err, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_SOCKET, + _("Connection timeout for the dictionary server at '%s:%d'"), + context->priv->hostname, + context->priv->port); + + g_signal_emit_by_name (context, "error", err); + + g_error_free (err); + + gdict_client_context_force_disconnect (context); + } + + /* this is a one-off operation */ + return FALSE; +} + +static gboolean +gdict_client_context_connect (GdictClientContext *context, + GError **error) +{ + GdictClientContextPrivate *priv; + GError *lookup_error, *flags_error; + gboolean res; + gint sock_fd, sock_res; + gsize addrlen; + GIOFlags flags; + + g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), FALSE); + + priv = context->priv; + + if (!priv->hostname) + { + g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_LOOKUP, + _("No hostname defined for the dictionary server")); + + return FALSE; + } + + /* forgive the absence of a port */ + if (!priv->port) + priv->port = GDICT_DEFAULT_PORT; + + priv->is_connecting = TRUE; + + lookup_error = NULL; + res = gdict_client_context_lookup_server (context, &lookup_error); + if (!res) + { + g_propagate_error (error, lookup_error); + + return FALSE; + } + +#ifdef ENABLE_IPV6 + if (priv->sockaddr.ss_family == AF_INET6) + ((struct sockaddr_in6 *) &priv->sockaddr)->sin6_port = g_htons (priv->port); + else +#endif + ((struct sockaddr_in *) &priv->sockaddr)->sin_port = g_htons (priv->port); + + +#ifdef ENABLE_IPV6 + if (priv->sockaddr.ss_family == AF_INET6) + { + sock_fd = socket (AF_INET6, SOCK_STREAM, 0); + if (sock_fd < 0) + { + g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_SOCKET, + _("Unable to create socket")); + + return FALSE; + } + + addrlen = sizeof (struct sockaddr_in6); + } + else + { +#endif /* ENABLE_IPV6 */ + sock_fd = socket (AF_INET, SOCK_STREAM, 0); + if (sock_fd < 0) + { + g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_SOCKET, + _("Unable to create socket")); + + return FALSE; + } + + addrlen = sizeof (struct sockaddr_in); +#ifdef ENABLE_IPV6 + } +#endif + + priv->channel = g_io_channel_unix_new (sock_fd); + + /* RFC2229 mandates the usage of UTF-8, so we force this encoding */ + g_io_channel_set_encoding (priv->channel, "UTF-8", NULL); + + g_io_channel_set_line_term (priv->channel, "\r\n", 2); + + /* make sure that the channel is non-blocking */ + flags = g_io_channel_get_flags (priv->channel); + flags |= G_IO_FLAG_NONBLOCK; + flags_error = NULL; + g_io_channel_set_flags (priv->channel, flags, &flags_error); + if (flags_error) + { + g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_SOCKET, + _("Unable to set the channel as non-blocking: %s"), + flags_error->message); + + g_error_free (flags_error); + g_io_channel_unref (priv->channel); + + return FALSE; + } + + /* let the magic begin */ + sock_res = connect (sock_fd, (struct sockaddr *) &priv->sockaddr, addrlen); + if ((sock_res != 0) && (errno != EINPROGRESS)) + { + g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, + GDICT_CLIENT_CONTEXT_ERROR_SOCKET, + _("Unable to connect to the dictionary server at '%s:%d'"), + priv->hostname, + priv->port); + + return FALSE; + } + + priv->timeout_id = g_timeout_add (CONNECTION_TIMEOUT * 1000, + check_for_connection, + context); + + /* XXX - remember that g_io_add_watch() increases the reference count + * of the GIOChannel we are using. + */ + priv->source_id = g_io_add_watch (priv->channel, + (G_IO_IN | G_IO_ERR), + (GIOFunc) gdict_client_context_io_watch_cb, + context); + + return TRUE; +} + +static void +gdict_client_context_disconnect (GdictClientContext *context) +{ + GdictCommand *cmd; + + g_return_if_fail (GDICT_IS_CLIENT_CONTEXT (context)); + + /* instead of just breaking the connection to the server, we push + * a QUIT command on the queue, and wait for every other scheduled + * command to perform; this allows the creation of a batch of + * commands. + */ + cmd = gdict_command_new (CMD_QUIT); + cmd->state = S_FINISH; + + gdict_client_context_push_command (context, cmd); +} + +static gboolean +gdict_client_context_is_connected (GdictClientContext *context) +{ + g_assert (GDICT_IS_CLIENT_CONTEXT (context)); + + /* we are in the middle of a connection attempt */ + if (context->priv->is_connecting) + return TRUE; + + return (context->priv->channel != NULL && context->priv->source_id != 0); +} + +static gboolean +gdict_client_context_get_databases (GdictContext *context, + GError **error) +{ + GdictClientContext *client_ctx; + GdictCommand *cmd; + + g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), FALSE); + + client_ctx = GDICT_CLIENT_CONTEXT (context); + + if (!gdict_client_context_is_connected (client_ctx)) + { + GError *connect_error = NULL; + + gdict_client_context_connect (client_ctx, &connect_error); + if (connect_error) + { + g_propagate_error (error, connect_error); + + return FALSE; + } + } + + cmd = gdict_command_new (CMD_SHOW_DB); + + return gdict_client_context_push_command (client_ctx, cmd); +} + +static gboolean +gdict_client_context_get_strategies (GdictContext *context, + GError **error) +{ + GdictClientContext *client_ctx; + GdictCommand *cmd; + + g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), FALSE); + + client_ctx = GDICT_CLIENT_CONTEXT (context); + + if (!gdict_client_context_is_connected (client_ctx)) + { + GError *connect_error = NULL; + + gdict_client_context_connect (client_ctx, &connect_error); + if (connect_error) + { + g_propagate_error (error, connect_error); + + return FALSE; + } + } + + cmd = gdict_command_new (CMD_SHOW_STRAT); + + return gdict_client_context_push_command (client_ctx, cmd); +} + +static gboolean +gdict_client_context_define_word (GdictContext *context, + const gchar *database, + const gchar *word, + GError **error) +{ + GdictClientContext *client_ctx; + GdictCommand *cmd; + + g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), FALSE); + + client_ctx = GDICT_CLIENT_CONTEXT (context); + + if (!gdict_client_context_is_connected (client_ctx)) + { + GError *connect_error = NULL; + + gdict_client_context_connect (client_ctx, &connect_error); + if (connect_error) + { + g_propagate_error (error, connect_error); + + return FALSE; + } + } + + cmd = gdict_command_new (CMD_DEFINE); + cmd->database = g_strdup ((database != NULL ? database : GDICT_DEFAULT_DATABASE)); + cmd->word = g_utf8_normalize (word, -1, G_NORMALIZE_NFC); + + return gdict_client_context_push_command (client_ctx, cmd); +} + +static gboolean +gdict_client_context_match_word (GdictContext *context, + const gchar *database, + const gchar *strategy, + const gchar *word, + GError **error) +{ + GdictClientContext *client_ctx; + GdictCommand *cmd; + + g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), FALSE); + + client_ctx = GDICT_CLIENT_CONTEXT (context); + + if (!gdict_client_context_is_connected (client_ctx)) + { + GError *connect_error = NULL; + + gdict_client_context_connect (client_ctx, &connect_error); + if (connect_error) + { + g_propagate_error (error, connect_error); + + return FALSE; + } + } + + cmd = gdict_command_new (CMD_MATCH); + cmd->database = g_strdup ((database != NULL ? database : GDICT_DEFAULT_DATABASE)); + cmd->strategy = g_strdup ((strategy != NULL ? strategy : GDICT_DEFAULT_STRATEGY)); + cmd->word = g_utf8_normalize (word, -1, G_NORMALIZE_NFC); + + return gdict_client_context_push_command (client_ctx, cmd); +} |