degesch: more multiserver preparations

Almost done now.
This commit is contained in:
Přemysl Eric Janouch 2015-05-16 12:33:59 +02:00
parent bedbadd396
commit ca8540e217
1 changed files with 191 additions and 97 deletions

288
degesch.c
View File

@ -1013,10 +1013,18 @@ struct server
{ {
struct app_context *ctx; ///< Application context struct app_context *ctx; ///< Application context
char *name; ///< Server identifier
struct buffer *buffer; ///< The buffer for this server
// Configuration:
struct config_item_ *config; ///< Configuration root
bool reconnect; ///< Whether to reconnect on conn. fail. bool reconnect; ///< Whether to reconnect on conn. fail.
unsigned long reconnect_delay; ///< Reconnect delay in seconds unsigned long reconnect_delay; ///< Reconnect delay in seconds
bool manual_disconnect; ///< Don't reconnect bool manual_disconnect; ///< Don't reconnect
// Connection:
enum server_state state; ///< Connection state enum server_state state; ///< Connection state
struct connector *connector; ///< Connection establisher struct connector *connector; ///< Connection establisher
@ -1030,8 +1038,7 @@ struct server
// TODO: an output queue to prevent excess floods (this will be needed // TODO: an output queue to prevent excess floods (this will be needed
// especially for away status polling) // especially for away status polling)
char *name; ///< Server identifier // IRC:
struct buffer *buffer; ///< The buffer for this server
struct str_map irc_users; ///< IRC user data struct str_map irc_users; ///< IRC user data
struct str_map irc_channels; ///< IRC channel data struct str_map irc_channels; ///< IRC channel data
@ -1055,6 +1062,8 @@ static void irc_initiate_connect (struct server *s);
static void static void
server_init (struct server *self, struct poller *poller) server_init (struct server *self, struct poller *poller)
{ {
memset (self, 0, sizeof *self);
self->socket = -1; self->socket = -1;
str_init (&self->read_buffer); str_init (&self->read_buffer);
self->state = IRC_DISCONNECTED; self->state = IRC_DISCONNECTED;
@ -1111,6 +1120,13 @@ server_free (struct server *self)
str_map_free (&self->irc_buffer_map); str_map_free (&self->irc_buffer_map);
} }
static void
server_destroy (void *self)
{
server_free (self);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct app_context struct app_context
@ -1122,7 +1138,7 @@ struct app_context
bool no_colors; ///< Colour output mode bool no_colors; ///< Colour output mode
bool isolate_buffers; ///< Isolate global/server buffers bool isolate_buffers; ///< Isolate global/server buffers
struct server server; ///< Our only server so far struct str_map servers; ///< Our servers
// Events: // Events:
@ -1138,14 +1154,13 @@ struct app_context
struct buffer *buffers; ///< All our buffers in order struct buffer *buffers; ///< All our buffers in order
struct buffer *buffers_tail; ///< The tail of our buffers struct buffer *buffers_tail; ///< The tail of our buffers
struct buffer *global_buffer; ///< The global buffer
struct buffer *current_buffer; ///< The current buffer
struct buffer *last_buffer; ///< Last used buffer struct buffer *last_buffer; ///< Last used buffer
// TODO: make buffer names fully unique like weechat does // TODO: make buffer names fully unique like weechat does
struct str_map buffers_by_name; ///< Buffers by name struct str_map buffers_by_name; ///< Buffers by name
struct buffer *global_buffer; ///< The global buffer
struct buffer *current_buffer; ///< The current buffer
// TODO: So that we always output proper date change messages // TODO: So that we always output proper date change messages
time_t last_displayed_msg_time; ///< Time of last displayed message time_t last_displayed_msg_time; ///< Time of last displayed message
@ -1171,9 +1186,9 @@ app_context_init (struct app_context *self)
config_init (&self->config); config_init (&self->config);
poller_init (&self->poller); poller_init (&self->poller);
server_init (&self->server, &self->poller); str_map_init (&self->servers);
self->server.ctx = self; self->servers.free = server_destroy;
self->server.name = xstrdup ("server"); self->servers.key_xfrm = irc_strxfrm;
str_map_init (&self->buffers_by_name); str_map_init (&self->buffers_by_name);
self->buffers_by_name.key_xfrm = irc_strxfrm; self->buffers_by_name.key_xfrm = irc_strxfrm;
@ -1213,7 +1228,7 @@ app_context_free (struct app_context *self)
buffer_destroy (iter); buffer_destroy (iter);
str_map_free (&self->buffers_by_name); str_map_free (&self->buffers_by_name);
server_free (&self->server); str_map_free (&self->servers);
poller_free (&self->poller); poller_free (&self->poller);
iconv_close (self->latin1_to_utf8); iconv_close (self->latin1_to_utf8);
@ -1415,9 +1430,9 @@ register_config_modules (struct app_context *ctx)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const char * static const char *
get_config_string (struct app_context *ctx, const char *key) get_config_string (struct config_item_ *root, const char *key)
{ {
struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); struct config_item_ *item = config_item_get (root, key, NULL);
hard_assert (item); hard_assert (item);
if (item->type == CONFIG_ITEM_NULL) if (item->type == CONFIG_ITEM_NULL)
return NULL; return NULL;
@ -1426,9 +1441,10 @@ get_config_string (struct app_context *ctx, const char *key)
} }
static bool static bool
set_config_string (struct app_context *ctx, const char *key, const char *value) set_config_string
(struct config_item_ *root, const char *key, const char *value)
{ {
struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); struct config_item_ *item = config_item_get (root, key, NULL);
hard_assert (item); hard_assert (item);
struct str s; struct str s;
@ -1448,17 +1464,17 @@ set_config_string (struct app_context *ctx, const char *key, const char *value)
} }
static int64_t static int64_t
get_config_integer (struct app_context *ctx, const char *key) get_config_integer (struct config_item_ *root, const char *key)
{ {
struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); struct config_item_ *item = config_item_get (root, key, NULL);
hard_assert (item && item->type == CONFIG_ITEM_INTEGER); hard_assert (item && item->type == CONFIG_ITEM_INTEGER);
return item->value.integer; return item->value.integer;
} }
static bool static bool
get_config_boolean (struct app_context *ctx, const char *key) get_config_boolean (struct config_item_ *root, const char *key)
{ {
struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); struct config_item_ *item = config_item_get (root, key, NULL);
hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN); hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN);
return item->value.boolean; return item->value.boolean;
} }
@ -1687,7 +1703,7 @@ init_attribute (struct app_context *ctx, int id, const char *default_)
#undef XX #undef XX
}; };
const char *user = get_config_string (ctx, table[id]); const char *user = get_config_string (ctx->config.root, table[id]);
if (user) if (user)
ctx->attrs[id] = xstrdup (user); ctx->attrs[id] = xstrdup (user);
else else
@ -2483,19 +2499,19 @@ buffer_remove (struct app_context *ctx, struct buffer *buffer)
if (buffer->user) if (buffer->user)
str_map_set (&s->irc_buffer_map, buffer->user->nickname, NULL); str_map_set (&s->irc_buffer_map, buffer->user->nickname, NULL);
str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
buffer_destroy (buffer);
if (buffer == ctx->last_buffer)
ctx->last_buffer = NULL;
// It's not a good idea to remove these buffers, but it's even a worse // It's not a good idea to remove these buffers, but it's even a worse
// one to leave the pointers point to invalid memory // one to leave the pointers point to invalid memory
if (buffer == ctx->global_buffer) if (buffer == ctx->global_buffer)
ctx->global_buffer = NULL; ctx->global_buffer = NULL;
if (buffer == ctx->server.buffer) if (buffer->type == BUFFER_SERVER)
ctx->server.buffer = NULL; buffer->server->buffer = NULL;
if (buffer == ctx->last_buffer)
ctx->last_buffer = NULL;
str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
buffer_destroy (buffer);
refresh_prompt (ctx); refresh_prompt (ctx);
} }
@ -2635,19 +2651,11 @@ buffer_get_index (struct app_context *ctx, struct buffer *buffer)
static void static void
init_buffers (struct app_context *ctx) init_buffers (struct app_context *ctx)
{ {
// At the moment we have only two global everpresent buffers
struct buffer *global = ctx->global_buffer = buffer_new (); struct buffer *global = ctx->global_buffer = buffer_new ();
struct buffer *server = ctx->server.buffer = buffer_new ();
global->type = BUFFER_GLOBAL; global->type = BUFFER_GLOBAL;
global->name = xstrdup (PROGRAM_NAME); global->name = xstrdup (PROGRAM_NAME);
server->type = BUFFER_SERVER;
server->name = xstrdup ("server");
server->server = &ctx->server;
buffer_add (ctx, global); buffer_add (ctx, global);
buffer_add (ctx, server);
} }
// --- Users, channels --------------------------------------------------------- // --- Users, channels ---------------------------------------------------------
@ -2811,12 +2819,12 @@ irc_initialize_ssl_ctx (struct server *s, struct error **e)
{ {
// XXX: maybe we should call SSL_CTX_set_options() for some workarounds // XXX: maybe we should call SSL_CTX_set_options() for some workarounds
bool verify = get_config_boolean (s->ctx, "server.ssl_verify"); bool verify = get_config_boolean (s->config, "ssl_verify");
if (!verify) if (!verify)
SSL_CTX_set_verify (s->ssl_ctx, SSL_VERIFY_NONE, NULL); SSL_CTX_set_verify (s->ssl_ctx, SSL_VERIFY_NONE, NULL);
const char *ca_file = get_config_string (s->ctx, "server.ca_file"); const char *ca_file = get_config_string (s->config, "ssl_ca_file");
const char *ca_path = get_config_string (s->ctx, "server.ca_path"); const char *ca_path = get_config_string (s->config, "ssl_ca_path");
struct error *error = NULL; struct error *error = NULL;
if (ca_file || ca_path) if (ca_file || ca_path)
@ -2866,7 +2874,7 @@ irc_initialize_ssl (struct server *s, struct error **e)
if (!s->ssl) if (!s->ssl)
goto error_ssl_2; goto error_ssl_2;
const char *ssl_cert = get_config_string (s->ctx, "server.ssl_cert"); const char *ssl_cert = get_config_string (s->config, "ssl_cert");
if (ssl_cert) if (ssl_cert)
{ {
char *path = resolve_config_filename (ssl_cert); char *path = resolve_config_filename (ssl_cert);
@ -3011,8 +3019,19 @@ irc_destroy_connector (struct server *s)
static void static void
try_finish_quit (struct app_context *ctx) try_finish_quit (struct app_context *ctx)
{ {
// TODO: multiserver if (!ctx->quitting)
if (ctx->quitting && !irc_is_connected (&ctx->server)) return;
struct str_map_iter iter;
str_map_iter_init (&iter, &ctx->servers);
bool disconnected_all = true;
struct server *s;
while ((s = str_map_iter_next (&iter)))
if (irc_is_connected (s))
disconnected_all = false;
if (disconnected_all)
ctx->polling = false; ctx->polling = false;
} }
@ -3025,13 +3044,18 @@ initiate_quit (struct app_context *ctx)
buffer_send_status (ctx, ctx->global_buffer, "Shutting down"); buffer_send_status (ctx, ctx->global_buffer, "Shutting down");
// Initiate a connection close // Initiate a connection close
// TODO: multiserver struct str_map_iter iter;
struct server *s = &ctx->server; str_map_iter_init (&iter, &ctx->servers);
if (irc_is_connected (s))
// XXX: when we go async, we'll have to flush output buffers first struct server *s;
irc_shutdown (s); while ((s = str_map_iter_next (&iter)))
else if (s->state == IRC_CONNECTING) {
irc_destroy_connector (s); if (irc_is_connected (s))
// XXX: when we go async, we'll have to flush output buffers first
irc_shutdown (s);
else if (s->state == IRC_CONNECTING)
irc_destroy_connector (s);
}
ctx->quitting = true; ctx->quitting = true;
try_finish_quit (ctx); try_finish_quit (ctx);
@ -3248,9 +3272,9 @@ end:
static void static void
irc_register (struct server *s) irc_register (struct server *s)
{ {
const char *nickname = get_config_string (s->ctx, "server.nickname"); const char *nickname = get_config_string (s->config, "nickname");
const char *username = get_config_string (s->ctx, "server.username"); const char *username = get_config_string (s->config, "username");
const char *realname = get_config_string (s->ctx, "server.realname"); const char *realname = get_config_string (s->config, "realname");
// These are filled automatically if needed // These are filled automatically if needed
hard_assert (nickname && username && realname); hard_assert (nickname && username && realname);
@ -3268,7 +3292,7 @@ irc_finish_connection (struct server *s, int socket)
s->socket = socket; s->socket = socket;
struct error *e = NULL; struct error *e = NULL;
bool use_ssl = get_config_boolean (ctx, "server.ssl"); bool use_ssl = get_config_boolean (s->config, "ssl");
if (use_ssl && !irc_initialize_ssl (s, &e)) if (use_ssl && !irc_initialize_ssl (s, &e))
{ {
buffer_send_error (ctx, s->buffer, "Connection failed: %s", e->message); buffer_send_error (ctx, s->buffer, "Connection failed: %s", e->message);
@ -3378,13 +3402,13 @@ irc_initiate_connect_socks (struct server *s,
{ {
struct app_context *ctx = s->ctx; struct app_context *ctx = s->ctx;
const char *socks_host = get_config_string (ctx, "server.socks_host"); const char *socks_host = get_config_string (s->config, "socks_host");
int64_t socks_port_int = get_config_integer (ctx, "server.socks_port"); int64_t socks_port_int = get_config_integer (s->config, "socks_port");
const char *socks_username = const char *socks_username =
get_config_string (ctx, "server.socks_username"); get_config_string (s->config, "socks_username");
const char *socks_password = const char *socks_password =
get_config_string (ctx, "server.socks_password"); get_config_string (s->config, "socks_password");
if (!socks_host) if (!socks_host)
return false; return false;
@ -3427,7 +3451,7 @@ irc_initiate_connect (struct server *s)
hard_assert (s->state == IRC_DISCONNECTED); hard_assert (s->state == IRC_DISCONNECTED);
struct app_context *ctx = s->ctx; struct app_context *ctx = s->ctx;
const char *addresses = get_config_string (ctx, "server.addresses"); const char *addresses = get_config_string (s->config, "addresses");
if (!addresses || !addresses[strspn (addresses, ",")]) if (!addresses || !addresses[strspn (addresses, ",")])
{ {
// No sense in trying to reconnect // No sense in trying to reconnect
@ -4231,7 +4255,7 @@ irc_on_registered (struct server *s, const char *nickname)
// we can also use WHOIS if it's not supported (optional by RFC 2812) // we can also use WHOIS if it's not supported (optional by RFC 2812)
irc_send (s, "USERHOST %s", s->irc_user->nickname); irc_send (s, "USERHOST %s", s->irc_user->nickname);
const char *autojoin = get_config_string (s->ctx, "server.autojoin"); const char *autojoin = get_config_string (s->config, "autojoin");
if (autojoin) if (autojoin)
irc_send (s, "JOIN :%s", autojoin); irc_send (s, "JOIN :%s", autojoin);
@ -5098,10 +5122,16 @@ handle_command_me (struct app_context *ctx, char *arguments)
static bool static bool
handle_command_quit (struct app_context *ctx, char *arguments) handle_command_quit (struct app_context *ctx, char *arguments)
{ {
// TODO: multiserver struct str_map_iter iter;
struct server *s = &ctx->server; str_map_iter_init (&iter, &ctx->servers);
if (irc_is_connected (s))
irc_initiate_disconnect (s, *arguments ? arguments : NULL); struct server *s;
while ((s = str_map_iter_next (&iter)))
{
if (irc_is_connected (s))
irc_initiate_disconnect (s, *arguments ? arguments : NULL);
}
initiate_quit (ctx); initiate_quit (ctx);
return true; return true;
} }
@ -5159,8 +5189,24 @@ handle_command_part (struct app_context *ctx, char *arguments)
static bool static bool
handle_command_connect (struct app_context *ctx, char *arguments) handle_command_connect (struct app_context *ctx, char *arguments)
{ {
// TODO: multiserver struct server *s = NULL;
struct server *s = &ctx->server; if (*arguments)
{
char *name = cut_word (&arguments);
if (!(s = str_map_find (&ctx->servers, name)))
buffer_send_error (ctx, ctx->global_buffer,
"%s: %s: %s", "Can't connect", "no such server", name);
}
else if (ctx->current_buffer->type == BUFFER_GLOBAL)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't connect",
"no server name given and this buffer is global");
else
s = ctx->current_buffer->server;
if (!s)
return true;
if (irc_is_connected (s)) if (irc_is_connected (s))
{ {
buffer_send_error (ctx, s->buffer, "Already connected"); buffer_send_error (ctx, s->buffer, "Already connected");
@ -5177,8 +5223,17 @@ handle_command_connect (struct app_context *ctx, char *arguments)
static bool static bool
handle_command_disconnect (struct app_context *ctx, char *arguments) handle_command_disconnect (struct app_context *ctx, char *arguments)
{ {
// TODO: multiserver // TODO: try to take server name from arguments
struct server *s = &ctx->server; struct server *s = NULL;
if (ctx->current_buffer->type == BUFFER_GLOBAL)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't disconnect", "this buffer is global");
else
s = ctx->current_buffer->server;
if (!s)
return true;
if (s->state == IRC_CONNECTING) if (s->state == IRC_CONNECTING)
{ {
buffer_send_status (ctx, s->buffer, "Connecting aborted"); buffer_send_status (ctx, s->buffer, "Connecting aborted");
@ -5292,7 +5347,7 @@ g_command_handlers[] =
NOT_IMPLEMENTED (invite) NOT_IMPLEMENTED (invite)
{ "connect", "Connect to the server", { "connect", "Connect to the server",
NULL, "[server]",
handle_command_connect }, handle_command_connect },
{ "disconnect", "Disconnect from the server", { "disconnect", "Disconnect from the server",
"[reason]", "[reason]",
@ -6264,11 +6319,11 @@ app_editline_init (struct input *self)
// --- Configuration loading --------------------------------------------------- // --- Configuration loading ---------------------------------------------------
static bool static bool
autofill_user_info (struct app_context *ctx, struct error **e) autofill_user_info (struct server *s, struct error **e)
{ {
const char *nickname = get_config_string (ctx, "server.nickname"); const char *nickname = get_config_string (s->config, "nickname");
const char *username = get_config_string (ctx, "server.username"); const char *username = get_config_string (s->config, "username");
const char *realname = get_config_string (ctx, "server.realname"); const char *realname = get_config_string (s->config, "realname");
if (nickname && username && realname) if (nickname && username && realname)
return true; return true;
@ -6280,9 +6335,9 @@ autofill_user_info (struct app_context *ctx, struct error **e)
// FIXME: set_config_strings() writes errors on its own // FIXME: set_config_strings() writes errors on its own
if (!nickname) if (!nickname)
set_config_string (ctx, "server.nickname", pwd->pw_name); set_config_string (s->config, "nickname", pwd->pw_name);
if (!username) if (!username)
set_config_string (ctx, "server.username", pwd->pw_name); set_config_string (s->config, "username", pwd->pw_name);
// Not all systems have the GECOS field but the vast majority does // Not all systems have the GECOS field but the vast majority does
if (!realname) if (!realname)
@ -6294,7 +6349,7 @@ autofill_user_info (struct app_context *ctx, struct error **e)
if (comma) if (comma)
*comma = '\0'; *comma = '\0';
set_config_string (ctx, "server.realname", gecos); set_config_string (s->config, "realname", gecos);
} }
return true; return true;
@ -6371,18 +6426,9 @@ load_configuration (struct app_context *ctx)
} }
config_load (&ctx->config, root ? root : config_item_object ()); config_load (&ctx->config, root ? root : config_item_object ());
if (!autofill_user_info (ctx, &e))
{
print_error ("%s: %s", "failed to fill in user details", e->message);
error_free (e);
}
ctx->isolate_buffers = ctx->isolate_buffers =
get_config_boolean (ctx, "behaviour.isolate_buffers"); get_config_boolean (ctx->config.root, "behaviour.isolate_buffers");
ctx->server.reconnect =
get_config_boolean (ctx, "server.reconnect");
ctx->server.reconnect_delay =
get_config_integer (ctx, "server.reconnect_delay");
} }
// --- Signals ----------------------------------------------------------------- // --- Signals -----------------------------------------------------------------
@ -6453,6 +6499,26 @@ setup_signal_handlers (void)
// --- I/O event handlers ------------------------------------------------------ // --- I/O event handlers ------------------------------------------------------
// FIXME: merge this with initiate_quit()
static void
preinitiate_quit (struct app_context *ctx)
{
struct str_map_iter iter;
str_map_iter_init (&iter, &ctx->servers);
struct server *s;
while ((s = str_map_iter_next (&iter)))
{
// There may be a timer set to reconnect to the server
// TODO: a faster timer for quitting
// XXX: why do we do this? Just to reset the reconnect timer?
irc_reset_connection_timeouts (s);
if (irc_is_connected (s))
irc_initiate_disconnect (s, NULL);
}
}
static void static void
on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx) on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
{ {
@ -6461,15 +6527,7 @@ on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
if (g_termination_requested && !ctx->quitting) if (g_termination_requested && !ctx->quitting)
{ {
// There may be a timer set to reconnect to the server preinitiate_quit (ctx);
// TODO: multiserver
struct server *s = &ctx->server;
// TODO: a faster timer for quitting
// XXX: why do we do this? Just to reset the reconnect timer?
irc_reset_connection_timeouts (s);
if (irc_is_connected (s))
irc_initiate_disconnect (s, NULL);
initiate_quit (ctx); initiate_quit (ctx);
} }
@ -6588,6 +6646,43 @@ display_logo (void)
str_vector_free (&v); str_vector_free (&v);
} }
static void
create_server (struct app_context *ctx)
{
struct server *s = xmalloc (sizeof *s);
server_init (s, &ctx->poller);
s->ctx = ctx;
s->name = xstrdup ("server");
str_map_set (&ctx->servers, s->name, s);
// Load configuration
s->config = config_item_get (ctx->config.root, "server", NULL);
hard_assert (s->config != NULL);
s->reconnect = get_config_boolean (s->config, "reconnect");
s->reconnect_delay = get_config_integer (s->config, "reconnect_delay");
struct error *e = NULL;
if (!autofill_user_info (s, &e))
{
print_error ("%s: %s", "failed to fill in user details", e->message);
error_free (e);
}
// Add a buffer and activate it
struct buffer *buffer = s->buffer = buffer_new ();
buffer->type = BUFFER_SERVER;
buffer->name = xstrdup (s->name);
buffer->server = s;
buffer_add (ctx, buffer);
buffer_activate (ctx, buffer);
// Connect to the server ASAP
poller_timer_set (&s->reconnect_tmr, 0);
}
int int
main (int argc, char *argv[]) main (int argc, char *argv[])
{ {
@ -6650,10 +6745,9 @@ main (int argc, char *argv[])
refresh_prompt (&ctx); refresh_prompt (&ctx);
input_start (&ctx.input, argv[0]); input_start (&ctx.input, argv[0]);
buffer_activate (&ctx, ctx.server.buffer);
// Connect to the server ASAP // TODO: finish multi-server
poller_timer_set (&ctx.server.reconnect_tmr, 0); create_server (&ctx);
ctx.polling = true; ctx.polling = true;
while (ctx.polling) while (ctx.polling)