mirror of
https://gitlab.freedesktop.org/wayland/wayland.git
synced 2026-05-03 06:46:33 -04:00
Merge branch 'spawn_zombies' into 'main'
Draft: client: Don't disconnect on receipt of object events destined for zombies Closes #74 See merge request wayland/wayland!173
This commit is contained in:
commit
3bc3dc8c52
5 changed files with 218 additions and 107 deletions
|
|
@ -49,4 +49,35 @@
|
||||||
<arg name="passer" type="object" interface="fd_passer"/>
|
<arg name="passer" type="object" interface="fd_passer"/>
|
||||||
</request>
|
</request>
|
||||||
</interface>
|
</interface>
|
||||||
|
|
||||||
|
<interface name="noop" version="1">
|
||||||
|
<description summary="An interface that does nothing">
|
||||||
|
A trivial interface.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor"/>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="necromancer" version="1">
|
||||||
|
<description summary="Helper for creating zombie objects">
|
||||||
|
Simple interface for generating server side objects to use in
|
||||||
|
zombie tests.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor"/>
|
||||||
|
|
||||||
|
<request name="create_server_object">
|
||||||
|
<description summary="create a new server side object">
|
||||||
|
Create a new server side object.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="server_object">
|
||||||
|
<description summary="server side object">
|
||||||
|
</description>
|
||||||
|
<arg name="id" type="new_id" interface="noop" summary="server side object"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
|
||||||
|
</interface>
|
||||||
</protocol>
|
</protocol>
|
||||||
|
|
|
||||||
|
|
@ -200,12 +200,6 @@ close_fds(struct wl_ring_buffer *buffer, int max)
|
||||||
buffer->tail += size;
|
buffer->tail += size;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
wl_connection_close_fds_in(struct wl_connection *connection, int max)
|
|
||||||
{
|
|
||||||
close_fds(&connection->fds_in, max);
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
int
|
||||||
wl_connection_destroy(struct wl_connection *connection)
|
wl_connection_destroy(struct wl_connection *connection)
|
||||||
{
|
{
|
||||||
|
|
@ -809,7 +803,8 @@ wl_connection_demarshal(struct wl_connection *connection,
|
||||||
goto err;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wl_map_reserve_new(objects, id) < 0) {
|
if (wl_map_reserve_new(objects, id) < 0 &&
|
||||||
|
!wl_object_is_zombie(objects, id)) {
|
||||||
wl_log("not a valid new object id (%u), "
|
wl_log("not a valid new object id (%u), "
|
||||||
"message %s(%s)\n",
|
"message %s(%s)\n",
|
||||||
id, message->name, message->signature);
|
id, message->name, message->signature);
|
||||||
|
|
@ -879,10 +874,6 @@ wl_object_is_zombie(struct wl_map *map, uint32_t id)
|
||||||
if (map->side == WL_MAP_SERVER_SIDE)
|
if (map->side == WL_MAP_SERVER_SIDE)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
/* Zombie objects can only have been created by the client. */
|
|
||||||
if (id >= WL_SERVER_ID_START)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
flags = wl_map_lookup_flags(map, id);
|
flags = wl_map_lookup_flags(map, id);
|
||||||
return !!(flags & WL_MAP_ENTRY_ZOMBIE);
|
return !!(flags & WL_MAP_ENTRY_ZOMBIE);
|
||||||
}
|
}
|
||||||
|
|
@ -909,8 +900,7 @@ wl_closure_lookup_objects(struct wl_closure *closure, struct wl_map *objects)
|
||||||
|
|
||||||
object = wl_map_lookup(objects, id);
|
object = wl_map_lookup(objects, id);
|
||||||
if (wl_object_is_zombie(objects, id)) {
|
if (wl_object_is_zombie(objects, id)) {
|
||||||
/* references object we've already
|
/* references object we've already destroyed */
|
||||||
* destroyed client side */
|
|
||||||
object = NULL;
|
object = NULL;
|
||||||
} else if (object == NULL && id != 0) {
|
} else if (object == NULL && id != 0) {
|
||||||
wl_log("unknown object (%u), message %s(%s)\n",
|
wl_log("unknown object (%u), message %s(%s)\n",
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,6 @@ enum wl_proxy_flag {
|
||||||
WL_PROXY_FLAG_WRAPPER = (1 << 2),
|
WL_PROXY_FLAG_WRAPPER = (1 << 2),
|
||||||
};
|
};
|
||||||
|
|
||||||
struct wl_zombie {
|
|
||||||
int event_count;
|
|
||||||
int *fd_count;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct wl_proxy {
|
struct wl_proxy {
|
||||||
struct wl_object object;
|
struct wl_object object;
|
||||||
struct wl_display *display;
|
struct wl_display *display;
|
||||||
|
|
@ -352,62 +347,11 @@ wl_display_create_queue(struct wl_display *display)
|
||||||
return queue;
|
return queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
|
||||||
message_count_fds(const char *signature)
|
|
||||||
{
|
|
||||||
unsigned int count, i, fds = 0;
|
|
||||||
struct argument_details arg;
|
|
||||||
|
|
||||||
count = arg_count_for_signature(signature);
|
|
||||||
for (i = 0; i < count; i++) {
|
|
||||||
signature = get_next_argument(signature, &arg);
|
|
||||||
if (arg.type == 'h')
|
|
||||||
fds++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fds;
|
|
||||||
}
|
|
||||||
|
|
||||||
static struct wl_zombie *
|
|
||||||
prepare_zombie(struct wl_proxy *proxy)
|
|
||||||
{
|
|
||||||
const struct wl_interface *interface = proxy->object.interface;
|
|
||||||
const struct wl_message *message;
|
|
||||||
int i, count;
|
|
||||||
struct wl_zombie *zombie = NULL;
|
|
||||||
|
|
||||||
/* If we hit an event with an FD, ensure we have a zombie object and
|
|
||||||
* fill the fd_count slot for that event with the number of FDs for
|
|
||||||
* that event. Interfaces with no events containing FDs will not have
|
|
||||||
* zombie objects created. */
|
|
||||||
for (i = 0; i < interface->event_count; i++) {
|
|
||||||
message = &interface->events[i];
|
|
||||||
count = message_count_fds(message->signature);
|
|
||||||
|
|
||||||
if (!count)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!zombie) {
|
|
||||||
zombie = zalloc(sizeof(*zombie) +
|
|
||||||
(interface->event_count * sizeof(int)));
|
|
||||||
if (!zombie)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
zombie->event_count = interface->event_count;
|
|
||||||
zombie->fd_count = (int *) &zombie[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
zombie->fd_count[i] = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return zombie;
|
|
||||||
}
|
|
||||||
|
|
||||||
static enum wl_iterator_result
|
static enum wl_iterator_result
|
||||||
free_zombies(void *element, void *data, uint32_t flags)
|
free_zombies(void *element, void *data, uint32_t flags)
|
||||||
{
|
{
|
||||||
if (flags & WL_MAP_ENTRY_ZOMBIE)
|
if (flags & WL_MAP_ENTRY_ZOMBIE)
|
||||||
free(element);
|
wl_proxy_unref(element);
|
||||||
|
|
||||||
return WL_ITERATOR_CONTINUE;
|
return WL_ITERATOR_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
@ -471,7 +415,7 @@ static struct wl_proxy *
|
||||||
wl_proxy_create_for_id(struct wl_proxy *factory,
|
wl_proxy_create_for_id(struct wl_proxy *factory,
|
||||||
uint32_t id, const struct wl_interface *interface)
|
uint32_t id, const struct wl_interface *interface)
|
||||||
{
|
{
|
||||||
struct wl_proxy *proxy;
|
struct wl_proxy *proxy, *zombie;
|
||||||
struct wl_display *display = factory->display;
|
struct wl_display *display = factory->display;
|
||||||
|
|
||||||
proxy = zalloc(sizeof *proxy);
|
proxy = zalloc(sizeof *proxy);
|
||||||
|
|
@ -484,6 +428,9 @@ wl_proxy_create_for_id(struct wl_proxy *factory,
|
||||||
proxy->queue = factory->queue;
|
proxy->queue = factory->queue;
|
||||||
proxy->refcount = 1;
|
proxy->refcount = 1;
|
||||||
proxy->version = factory->version;
|
proxy->version = factory->version;
|
||||||
|
zombie = wl_map_lookup(&display->objects, id);
|
||||||
|
if (zombie && wl_object_is_zombie(&display->objects, id))
|
||||||
|
wl_proxy_unref(zombie);
|
||||||
|
|
||||||
wl_map_insert_at(&display->objects, 0, id, proxy);
|
wl_map_insert_at(&display->objects, 0, id, proxy);
|
||||||
|
|
||||||
|
|
@ -493,25 +440,21 @@ wl_proxy_create_for_id(struct wl_proxy *factory,
|
||||||
static void
|
static void
|
||||||
proxy_destroy(struct wl_proxy *proxy)
|
proxy_destroy(struct wl_proxy *proxy)
|
||||||
{
|
{
|
||||||
|
proxy->flags |= WL_PROXY_FLAG_DESTROYED;
|
||||||
|
|
||||||
if (proxy->flags & WL_PROXY_FLAG_ID_DELETED) {
|
if (proxy->flags & WL_PROXY_FLAG_ID_DELETED) {
|
||||||
wl_map_remove(&proxy->display->objects, proxy->object.id);
|
wl_map_remove(&proxy->display->objects, proxy->object.id);
|
||||||
} else if (proxy->object.id < WL_SERVER_ID_START) {
|
wl_proxy_unref(proxy);
|
||||||
struct wl_zombie *zombie = prepare_zombie(proxy);
|
} else {
|
||||||
|
/* The map now contains the zombie entry. A delete_id
|
||||||
/* The map now contains the zombie entry, until the delete_id
|
* event will clean up client generated ids eventually,
|
||||||
* event arrives. */
|
* server generated ids will linger until they're
|
||||||
|
* overwritten. */
|
||||||
wl_map_insert_at(&proxy->display->objects,
|
wl_map_insert_at(&proxy->display->objects,
|
||||||
WL_MAP_ENTRY_ZOMBIE,
|
WL_MAP_ENTRY_ZOMBIE,
|
||||||
proxy->object.id,
|
proxy->object.id,
|
||||||
zombie);
|
proxy);
|
||||||
} else {
|
|
||||||
wl_map_insert_at(&proxy->display->objects, 0,
|
|
||||||
proxy->object.id, NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy->flags |= WL_PROXY_FLAG_DESTROYED;
|
|
||||||
|
|
||||||
wl_proxy_unref(proxy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
@ -1034,9 +977,7 @@ display_handle_delete_id(void *data, struct wl_display *display, uint32_t id)
|
||||||
proxy = wl_map_lookup(&display->objects, id);
|
proxy = wl_map_lookup(&display->objects, id);
|
||||||
|
|
||||||
if (wl_object_is_zombie(&display->objects, id)) {
|
if (wl_object_is_zombie(&display->objects, id)) {
|
||||||
/* For zombie objects, the 'proxy' is actually the zombie
|
wl_proxy_unref(proxy);
|
||||||
* event-information structure, which we can free. */
|
|
||||||
free(proxy);
|
|
||||||
wl_map_remove(&display->objects, id);
|
wl_map_remove(&display->objects, id);
|
||||||
} else if (proxy) {
|
} else if (proxy) {
|
||||||
proxy->flags |= WL_PROXY_FLAG_ID_DELETED;
|
proxy->flags |= WL_PROXY_FLAG_ID_DELETED;
|
||||||
|
|
@ -1411,6 +1352,7 @@ create_proxies(struct wl_proxy *sender, struct wl_closure *closure)
|
||||||
if (proxy == NULL)
|
if (proxy == NULL)
|
||||||
return -1;
|
return -1;
|
||||||
closure->args[i].o = (struct wl_object *)proxy;
|
closure->args[i].o = (struct wl_object *)proxy;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
@ -1447,6 +1389,30 @@ increase_closure_args_refcount(struct wl_closure *closure)
|
||||||
closure->proxy->refcount++;
|
closure->proxy->refcount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
inter_zombie_closure(struct wl_closure *closure)
|
||||||
|
{
|
||||||
|
const char *signature;
|
||||||
|
struct argument_details arg;
|
||||||
|
int i, count;
|
||||||
|
struct wl_proxy *proxy;
|
||||||
|
|
||||||
|
signature = closure->message->signature;
|
||||||
|
count = arg_count_for_signature(signature);
|
||||||
|
for (i = 0; i < count; i++) {
|
||||||
|
signature = get_next_argument(signature, &arg);
|
||||||
|
switch (arg.type) {
|
||||||
|
case 'n':
|
||||||
|
proxy = (struct wl_proxy *) closure->args[i].o;
|
||||||
|
if (proxy)
|
||||||
|
proxy_destroy(proxy);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
queue_event(struct wl_display *display, int len)
|
queue_event(struct wl_display *display, int len)
|
||||||
{
|
{
|
||||||
|
|
@ -1458,7 +1424,7 @@ queue_event(struct wl_display *display, int len)
|
||||||
struct wl_event_queue *queue;
|
struct wl_event_queue *queue;
|
||||||
struct timespec tp;
|
struct timespec tp;
|
||||||
unsigned int time;
|
unsigned int time;
|
||||||
int num_zombie_fds;
|
bool zombie = false;
|
||||||
|
|
||||||
wl_connection_copy(display->connection, p, sizeof p);
|
wl_connection_copy(display->connection, p, sizeof p);
|
||||||
id = p[0];
|
id = p[0];
|
||||||
|
|
@ -1467,29 +1433,18 @@ queue_event(struct wl_display *display, int len)
|
||||||
if (len < size)
|
if (len < size)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
/* If our proxy is gone or a zombie, just eat the event (and any FDs,
|
/* If our proxy is gone, just eat the event. */
|
||||||
* if applicable). */
|
|
||||||
proxy = wl_map_lookup(&display->objects, id);
|
proxy = wl_map_lookup(&display->objects, id);
|
||||||
if (!proxy || wl_object_is_zombie(&display->objects, id)) {
|
if (!proxy) {
|
||||||
struct wl_zombie *zombie = wl_map_lookup(&display->objects, id);
|
|
||||||
num_zombie_fds = (zombie && opcode < zombie->event_count) ?
|
|
||||||
zombie->fd_count[opcode] : 0;
|
|
||||||
|
|
||||||
if (debug_client) {
|
if (debug_client) {
|
||||||
clock_gettime(CLOCK_REALTIME, &tp);
|
clock_gettime(CLOCK_REALTIME, &tp);
|
||||||
time = (tp.tv_sec * 1000000L) + (tp.tv_nsec / 1000);
|
time = (tp.tv_sec * 1000000L) + (tp.tv_nsec / 1000);
|
||||||
|
|
||||||
fprintf(stderr, "[%7u.%03u] discarded [%s]@%d.[event %d]"
|
fprintf(stderr, "[%7u.%03u] discarded @%d.[event %d]"
|
||||||
"(%d fd, %d byte)\n",
|
"(%d byte)\n",
|
||||||
time / 1000, time % 1000,
|
time / 1000, time % 1000,
|
||||||
zombie ? "zombie" : "unknown",
|
id, opcode, size);
|
||||||
id, opcode,
|
|
||||||
num_zombie_fds, size);
|
|
||||||
}
|
}
|
||||||
if (num_zombie_fds > 0)
|
|
||||||
wl_connection_close_fds_in(display->connection,
|
|
||||||
num_zombie_fds);
|
|
||||||
|
|
||||||
wl_connection_consume(display->connection, size);
|
wl_connection_consume(display->connection, size);
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
@ -1516,6 +1471,13 @@ queue_event(struct wl_display *display, int len)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zombie = wl_object_is_zombie(&display->objects, id);
|
||||||
|
if (zombie) {
|
||||||
|
inter_zombie_closure(closure);
|
||||||
|
wl_closure_destroy(closure);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
closure->proxy = proxy;
|
closure->proxy = proxy;
|
||||||
increase_closure_args_refcount(closure);
|
increase_closure_args_refcount(closure);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,4 @@ zalloc(size_t s)
|
||||||
return calloc(1, s);
|
return calloc(1, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
wl_connection_close_fds_in(struct wl_connection *connection, int max);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1629,3 +1629,134 @@ TEST(global_remove)
|
||||||
|
|
||||||
display_destroy(d);
|
display_destroy(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct new_zombie_data {
|
||||||
|
struct necromancer *necromancer;
|
||||||
|
struct necromancer *necromancer_two;
|
||||||
|
struct noop *temp_noop;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
server_object(void *data, struct necromancer *necro, struct noop *noop)
|
||||||
|
{
|
||||||
|
struct new_zombie_data *nzd = data;
|
||||||
|
|
||||||
|
/* We should only see this once, since one of the two noop
|
||||||
|
* objects we create should be created as a zombie */
|
||||||
|
assert(!nzd->temp_noop);
|
||||||
|
nzd->temp_noop = noop;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct necromancer_listener necromancer_listener = {
|
||||||
|
server_object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
new_zombie_handle_globals(void *data, struct wl_registry *registry,
|
||||||
|
uint32_t id, const char *intf, uint32_t ver)
|
||||||
|
{
|
||||||
|
struct new_zombie_data *rg = data;
|
||||||
|
|
||||||
|
if (!strcmp(intf, "necromancer")) {
|
||||||
|
rg->necromancer = wl_registry_bind(registry, id, &necromancer_interface, 1);
|
||||||
|
rg->necromancer_two = wl_registry_bind(registry, id, &necromancer_interface, 1);
|
||||||
|
necromancer_add_listener(rg->necromancer, &necromancer_listener, NULL);
|
||||||
|
necromancer_add_listener(rg->necromancer_two, &necromancer_listener, rg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct wl_registry_listener new_zombie_registry_listener = {
|
||||||
|
new_zombie_handle_globals,
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
static void noop_destructor(struct wl_client *client, struct wl_resource *res)
|
||||||
|
{
|
||||||
|
wl_resource_destroy(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct noop_interface noop_server_interface = {
|
||||||
|
noop_destructor
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
send_new_object(struct wl_client *client, struct wl_resource *res)
|
||||||
|
{
|
||||||
|
struct wl_resource *new_res;
|
||||||
|
|
||||||
|
new_res = wl_resource_create(client, &noop_interface, 1, 0);
|
||||||
|
wl_resource_set_implementation(new_res, &noop_server_interface, NULL, NULL);
|
||||||
|
necromancer_send_server_object(res, new_res);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void necromancer_destructor(struct wl_client *client, struct wl_resource *res)
|
||||||
|
{
|
||||||
|
wl_resource_destroy(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct necromancer_interface necro_interface = {
|
||||||
|
necromancer_destructor,
|
||||||
|
send_new_object
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
bind_necromancer(struct wl_client *client, void *data,
|
||||||
|
uint32_t vers, uint32_t id)
|
||||||
|
{
|
||||||
|
struct wl_resource *res;
|
||||||
|
|
||||||
|
res = wl_resource_create(client, &necromancer_interface, vers, id);
|
||||||
|
wl_resource_set_implementation(res, &necro_interface, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
spawn_new_zombie(void *data)
|
||||||
|
{
|
||||||
|
struct new_zombie_data rg = {};
|
||||||
|
struct client *c = client_connect();
|
||||||
|
struct wl_registry *reg;
|
||||||
|
|
||||||
|
reg = wl_display_get_registry(c->wl_display);
|
||||||
|
wl_registry_add_listener(reg, &new_zombie_registry_listener, &rg);
|
||||||
|
|
||||||
|
/* Get the globals */
|
||||||
|
wl_display_roundtrip(c->wl_display);
|
||||||
|
|
||||||
|
wl_registry_destroy(reg);
|
||||||
|
|
||||||
|
necromancer_create_server_object(rg.necromancer);
|
||||||
|
necromancer_create_server_object(rg.necromancer_two);
|
||||||
|
necromancer_destroy(rg.necromancer);
|
||||||
|
/* We should now have two new_id events on the way,
|
||||||
|
* and the first one will be destined for a zombie
|
||||||
|
* when it arrives. That would violate the wl_map
|
||||||
|
* requirement that a new id can only be one higher
|
||||||
|
* than the current max id in the map if we simply
|
||||||
|
* dropped the first inbound object.
|
||||||
|
*/
|
||||||
|
|
||||||
|
wl_display_roundtrip(c->wl_display);
|
||||||
|
assert(rg.temp_noop);
|
||||||
|
noop_destroy(rg.temp_noop);
|
||||||
|
necromancer_destroy(rg.necromancer_two);
|
||||||
|
wl_display_roundtrip(c->wl_display);
|
||||||
|
client_disconnect(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(new_zombie)
|
||||||
|
{
|
||||||
|
struct display *d;
|
||||||
|
struct wl_global *g;
|
||||||
|
|
||||||
|
d = display_create();
|
||||||
|
g = wl_global_create(d->wl_display, &necromancer_interface,
|
||||||
|
1, d, bind_necromancer);
|
||||||
|
|
||||||
|
client_create_noarg(d, spawn_new_zombie);
|
||||||
|
|
||||||
|
display_run(d);
|
||||||
|
|
||||||
|
wl_global_destroy(g);
|
||||||
|
|
||||||
|
display_destroy(d);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue