wlroots/backend/drm/backend.c

308 lines
8.9 KiB
C
Raw Normal View History

2018-02-12 20:29:23 +00:00
#include <assert.h>
#include <errno.h>
#include <drm_fourcc.h>
2017-05-01 05:49:18 +00:00
#include <stdio.h>
2018-02-12 20:29:23 +00:00
#include <stdlib.h>
2017-05-02 01:00:25 +00:00
#include <string.h>
2018-02-12 20:29:23 +00:00
#include <unistd.h>
#include <wayland-server-core.h>
2017-06-04 23:30:37 +00:00
#include <wlr/backend/interface.h>
2018-02-12 20:29:23 +00:00
#include <wlr/backend/session.h>
2017-06-21 14:27:45 +00:00
#include <wlr/interfaces/wlr_output.h>
2017-06-21 16:10:07 +00:00
#include <wlr/util/log.h>
2018-02-12 20:29:23 +00:00
#include <xf86drm.h>
#include "backend/drm/drm.h"
2017-05-01 05:49:18 +00:00
2018-09-17 20:25:20 +00:00
struct wlr_drm_backend *get_drm_backend_from_backend(
struct wlr_backend *wlr_backend) {
assert(wlr_backend_is_drm(wlr_backend));
return (struct wlr_drm_backend *)wlr_backend;
}
static bool backend_start(struct wlr_backend *backend) {
2018-09-17 20:25:20 +00:00
struct wlr_drm_backend *drm = get_drm_backend_from_backend(backend);
scan_drm_connectors(drm, NULL);
2017-05-07 14:00:23 +00:00
return true;
}
2017-05-03 09:28:44 +00:00
static void backend_destroy(struct wlr_backend *backend) {
2017-09-30 09:22:26 +00:00
if (!backend) {
2017-05-07 14:00:23 +00:00
return;
}
2018-09-17 20:25:20 +00:00
struct wlr_drm_backend *drm = get_drm_backend_from_backend(backend);
2017-09-30 09:22:26 +00:00
struct wlr_drm_connector *conn, *next;
wl_list_for_each_safe(conn, next, &drm->connectors, link) {
destroy_drm_connector(conn);
2017-05-31 20:17:04 +00:00
}
2017-08-05 06:15:39 +00:00
wlr_backend_finish(backend);
struct wlr_drm_fb *fb, *fb_tmp;
wl_list_for_each_safe(fb, fb_tmp, &drm->fbs, link) {
drm_fb_destroy(fb);
}
2017-12-07 22:44:59 +00:00
wl_list_remove(&drm->display_destroy.link);
backend/drm, backend/libinput: listen to session destroy This fixes a heap-use-after-free when the session is destroyed before the backend during wl_display_destroy: ==1085==ERROR: AddressSanitizer: heap-use-after-free on address 0x614000000180 at pc 0x7f88e3590c2d bp 0x7ffdc4e33f90 sp 0x7ffdc4e33f80 READ of size 8 at 0x614000000180 thread T0 #0 0x7f88e3590c2c in find_device ../subprojects/wlroots/backend/session/session.c:192 #1 0x7f88e3590e85 in wlr_session_close_file ../subprojects/wlroots/backend/session/session.c:204 #2 0x7f88e357b80c in libinput_close_restricted ../subprojects/wlroots/backend/libinput/backend.c:24 #3 0x7f88e21af274 (/lib64/libinput.so.10+0x28274) #4 0x7f88e21aff1d (/lib64/libinput.so.10+0x28f1d) #5 0x7f88e219ddac (/lib64/libinput.so.10+0x16dac) #6 0x7f88e21b415d in libinput_unref (/lib64/libinput.so.10+0x2d15d) #7 0x7f88e357c9d6 in backend_destroy ../subprojects/wlroots/backend/libinput/backend.c:130 #8 0x7f88e3545a09 in wlr_backend_destroy ../subprojects/wlroots/backend/backend.c:50 #9 0x7f88e358981a in multi_backend_destroy ../subprojects/wlroots/backend/multi/backend.c:54 #10 0x7f88e358a059 in handle_display_destroy ../subprojects/wlroots/backend/multi/backend.c:107 #11 0x7f88e314acde (/lib64/libwayland-server.so.0+0x8cde) #12 0x7f88e314b466 in wl_display_destroy (/lib64/libwayland-server.so.0+0x9466) #13 0x559fefb52385 in main ../main.c:67 #14 0x7f88e2639152 in __libc_start_main (/lib64/libc.so.6+0x27152) #15 0x559fefb4297d in _start (/home/simon/src/glider/build/glider+0x2297d) 0x614000000180 is located 320 bytes inside of 416-byte region [0x614000000040,0x6140000001e0) freed by thread T0 here: #0 0x7f88e3d0a6b0 in __interceptor_free /build/gcc/src/gcc/libsanitizer/asan/asan_malloc_linux.cc:122 #1 0x7f88e35b51fb in logind_session_destroy ../subprojects/wlroots/backend/session/logind.c:270 #2 0x7f88e35905a4 in wlr_session_destroy ../subprojects/wlroots/backend/session/session.c:156 #3 0x7f88e358f440 in handle_display_destroy ../subprojects/wlroots/backend/session/session.c:65 #4 0x7f88e314acde (/lib64/libwayland-server.so.0+0x8cde) previously allocated by thread T0 here: #0 0x7f88e3d0acd8 in __interceptor_calloc /build/gcc/src/gcc/libsanitizer/asan/asan_malloc_linux.cc:153 #1 0x7f88e35b911c in logind_session_create ../subprojects/wlroots/backend/session/logind.c:746 #2 0x7f88e358f6b4 in wlr_session_create ../subprojects/wlroots/backend/session/session.c:91 #3 0x559fefb51ea6 in main ../main.c:20 #4 0x7f88e2639152 in __libc_start_main (/lib64/libc.so.6+0x27152)
2019-11-30 10:57:37 +00:00
wl_list_remove(&drm->session_destroy.link);
wl_list_remove(&drm->session_active.link);
wl_list_remove(&drm->parent_destroy.link);
wl_list_remove(&drm->dev_change.link);
wl_list_remove(&drm->dev_remove.link);
2017-12-07 22:44:59 +00:00
if (drm->parent) {
finish_drm_renderer(&drm->mgpu_renderer);
}
finish_drm_resources(drm);
free(drm->name);
wlr_session_close_file(drm->session, drm->dev);
2017-09-30 09:22:26 +00:00
wl_event_source_remove(drm->drm_event);
free(drm);
2017-05-07 14:00:23 +00:00
}
static clockid_t backend_get_presentation_clock(struct wlr_backend *backend) {
2018-10-01 20:44:33 +00:00
struct wlr_drm_backend *drm = get_drm_backend_from_backend(backend);
return drm->clock;
}
2020-12-04 15:47:42 +00:00
static int backend_get_drm_fd(struct wlr_backend *backend) {
struct wlr_drm_backend *drm = get_drm_backend_from_backend(backend);
if (drm->parent) {
return drm->parent->fd;
} else {
return drm->fd;
}
}
static uint32_t drm_backend_get_buffer_caps(struct wlr_backend *backend) {
return WLR_BUFFER_CAP_DMABUF;
}
static const struct wlr_backend_impl backend_impl = {
.start = backend_start,
.destroy = backend_destroy,
.get_presentation_clock = backend_get_presentation_clock,
2020-12-04 15:47:42 +00:00
.get_drm_fd = backend_get_drm_fd,
.get_buffer_caps = drm_backend_get_buffer_caps,
2017-05-07 14:00:23 +00:00
};
bool wlr_backend_is_drm(struct wlr_backend *b) {
return b->impl == &backend_impl;
}
static void handle_session_active(struct wl_listener *listener, void *data) {
2017-12-07 22:44:59 +00:00
struct wlr_drm_backend *drm =
wl_container_of(listener, drm, session_active);
struct wlr_session *session = drm->session;
if (session->active) {
2018-07-09 21:49:54 +00:00
wlr_log(WLR_INFO, "DRM fd resumed");
scan_drm_connectors(drm, NULL);
backend/drm: disable all CRTCs after VT switch When the user switches away from the VT where wlroots is running, the new DRM master may mutate the KMS state in an arbitrary manner. For instance, let's say wlroots uses the following connector/CRTC mapping: - CRTC 42 drives connector DP-1 - CRTC 43 drives connector DP-2 Then the new DRM master may swap the mapping like so: - CRTC 42 drives connector DP-2 - CRTC 43 drives connector DP-1 wlroots needs to restore its own state when the user switches back. Some state is attached to wlr_drm_crtc (e.g. current FB), so reading back and adopting the CRTC/connector mapping left by the previous DRM master would be complicated (this was the source of other bugs in the past, see [1]). With the previous logic, wlroots merely tries to restore the state of each connector one after the other. This fails in the scenario described above: the kernel refuses to use CRTC 42 for DP-1, because that CRTC is already in-use for DP-2. Unfortunately with the legacy uAPI it's not possible to restore the state in one go. We need to support both legacy and atomic uAPIs, so let's fix the bug for the legacy uAPI first, and then improve the situation for the atomic uAPI as a second step [2]. We need to disable the CRTCs we're going to switch the connectors for. This sounds complicated, so let's just disable all CRTCs to simplify. This causes a black screen because of the on/off modesets, but makes VT switch much more reliable, so I'll take it. [1]: https://gitlab.freedesktop.org/wlroots/wlroots/-/commit/c6d8a11d2c438d514473b1cbe20e5550e7227472 [2]: https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/3794 Closes: https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3342
2023-01-15 15:21:21 +00:00
// The previous DRM master leaves KMS in an undefined state. We need
// to restore out own state, but be careful to avoid invalid
// configurations. The connector/CRTC mapping may have changed, so
// first disable all CRTCs, then light up the ones we were using
// before the VT switch.
// TODO: use the atomic API to improve restoration after a VT switch
for (size_t i = 0; i < drm->num_crtcs; i++) {
struct wlr_drm_crtc *crtc = &drm->crtcs[i];
if (drmModeSetCrtc(drm->fd, crtc->id, 0, 0, 0, NULL, 0, NULL) != 0) {
wlr_log_errno(WLR_ERROR, "Failed to disable CRTC %"PRIu32" after VT switch",
crtc->id);
}
}
struct wlr_drm_connector *conn;
wl_list_for_each(conn, &drm->connectors, link) {
struct wlr_output_mode *mode = NULL;
uint32_t committed = WLR_OUTPUT_STATE_ENABLED;
if (conn->status != DRM_MODE_DISCONNECTED && conn->output.enabled
&& conn->output.current_mode != NULL) {
committed |= WLR_OUTPUT_STATE_MODE;
mode = conn->output.current_mode;
2017-10-22 21:44:24 +00:00
}
struct wlr_output_state state = {
.committed = committed,
.allow_artifacts = true,
.enabled = mode != NULL,
.mode_type = WLR_OUTPUT_STATE_MODE_FIXED,
.mode = mode,
};
if (!drm_connector_commit_state(conn, &state)) {
wlr_drm_conn_log(conn, WLR_ERROR, "Failed to restore state after VT switch");
}
}
} else {
2018-07-09 21:49:54 +00:00
wlr_log(WLR_INFO, "DRM fd paused");
2017-05-13 13:12:47 +00:00
}
}
static void handle_dev_change(struct wl_listener *listener, void *data) {
struct wlr_drm_backend *drm = wl_container_of(listener, drm, dev_change);
struct wlr_device_change_event *change = data;
2017-06-03 03:47:33 +00:00
if (!drm->session->active) {
return;
}
2017-06-04 05:43:34 +00:00
switch (change->type) {
case WLR_DEVICE_HOTPLUG:
wlr_log(WLR_DEBUG, "Received hotplug event for %s", drm->name);
scan_drm_connectors(drm, &change->hotplug);
break;
case WLR_DEVICE_LEASE:
wlr_log(WLR_DEBUG, "Received lease event for %s", drm->name);
scan_drm_leases(drm);
break;
default:
wlr_log(WLR_DEBUG, "Received unknown change event for %s", drm->name);
}
2017-06-02 00:29:10 +00:00
}
static void handle_dev_remove(struct wl_listener *listener, void *data) {
struct wlr_drm_backend *drm = wl_container_of(listener, drm, dev_remove);
wlr_log(WLR_INFO, "Destroying DRM backend for %s", drm->name);
backend_destroy(&drm->backend);
}
backend/drm, backend/libinput: listen to session destroy This fixes a heap-use-after-free when the session is destroyed before the backend during wl_display_destroy: ==1085==ERROR: AddressSanitizer: heap-use-after-free on address 0x614000000180 at pc 0x7f88e3590c2d bp 0x7ffdc4e33f90 sp 0x7ffdc4e33f80 READ of size 8 at 0x614000000180 thread T0 #0 0x7f88e3590c2c in find_device ../subprojects/wlroots/backend/session/session.c:192 #1 0x7f88e3590e85 in wlr_session_close_file ../subprojects/wlroots/backend/session/session.c:204 #2 0x7f88e357b80c in libinput_close_restricted ../subprojects/wlroots/backend/libinput/backend.c:24 #3 0x7f88e21af274 (/lib64/libinput.so.10+0x28274) #4 0x7f88e21aff1d (/lib64/libinput.so.10+0x28f1d) #5 0x7f88e219ddac (/lib64/libinput.so.10+0x16dac) #6 0x7f88e21b415d in libinput_unref (/lib64/libinput.so.10+0x2d15d) #7 0x7f88e357c9d6 in backend_destroy ../subprojects/wlroots/backend/libinput/backend.c:130 #8 0x7f88e3545a09 in wlr_backend_destroy ../subprojects/wlroots/backend/backend.c:50 #9 0x7f88e358981a in multi_backend_destroy ../subprojects/wlroots/backend/multi/backend.c:54 #10 0x7f88e358a059 in handle_display_destroy ../subprojects/wlroots/backend/multi/backend.c:107 #11 0x7f88e314acde (/lib64/libwayland-server.so.0+0x8cde) #12 0x7f88e314b466 in wl_display_destroy (/lib64/libwayland-server.so.0+0x9466) #13 0x559fefb52385 in main ../main.c:67 #14 0x7f88e2639152 in __libc_start_main (/lib64/libc.so.6+0x27152) #15 0x559fefb4297d in _start (/home/simon/src/glider/build/glider+0x2297d) 0x614000000180 is located 320 bytes inside of 416-byte region [0x614000000040,0x6140000001e0) freed by thread T0 here: #0 0x7f88e3d0a6b0 in __interceptor_free /build/gcc/src/gcc/libsanitizer/asan/asan_malloc_linux.cc:122 #1 0x7f88e35b51fb in logind_session_destroy ../subprojects/wlroots/backend/session/logind.c:270 #2 0x7f88e35905a4 in wlr_session_destroy ../subprojects/wlroots/backend/session/session.c:156 #3 0x7f88e358f440 in handle_display_destroy ../subprojects/wlroots/backend/session/session.c:65 #4 0x7f88e314acde (/lib64/libwayland-server.so.0+0x8cde) previously allocated by thread T0 here: #0 0x7f88e3d0acd8 in __interceptor_calloc /build/gcc/src/gcc/libsanitizer/asan/asan_malloc_linux.cc:153 #1 0x7f88e35b911c in logind_session_create ../subprojects/wlroots/backend/session/logind.c:746 #2 0x7f88e358f6b4 in wlr_session_create ../subprojects/wlroots/backend/session/session.c:91 #3 0x559fefb51ea6 in main ../main.c:20 #4 0x7f88e2639152 in __libc_start_main (/lib64/libc.so.6+0x27152)
2019-11-30 10:57:37 +00:00
static void handle_session_destroy(struct wl_listener *listener, void *data) {
struct wlr_drm_backend *drm =
wl_container_of(listener, drm, session_destroy);
backend_destroy(&drm->backend);
}
2017-12-07 22:44:59 +00:00
static void handle_display_destroy(struct wl_listener *listener, void *data) {
struct wlr_drm_backend *drm =
wl_container_of(listener, drm, display_destroy);
backend_destroy(&drm->backend);
2017-12-07 22:44:59 +00:00
}
static void handle_parent_destroy(struct wl_listener *listener, void *data) {
struct wlr_drm_backend *drm =
wl_container_of(listener, drm, parent_destroy);
backend_destroy(&drm->backend);
}
2017-05-07 14:00:23 +00:00
struct wlr_backend *wlr_drm_backend_create(struct wl_display *display,
struct wlr_session *session, struct wlr_device *dev,
struct wlr_backend *parent) {
assert(display && session && dev);
2017-10-01 06:22:47 +00:00
assert(!parent || wlr_backend_is_drm(parent));
2017-06-03 03:47:33 +00:00
char *name = drmGetDeviceNameFromFd2(dev->fd);
drmVersion *version = drmGetVersion(dev->fd);
2018-07-09 21:49:54 +00:00
wlr_log(WLR_INFO, "Initializing DRM backend for %s (%s)", name, version->name);
2017-06-03 03:47:33 +00:00
drmFreeVersion(version);
2017-09-30 09:22:26 +00:00
struct wlr_drm_backend *drm = calloc(1, sizeof(struct wlr_drm_backend));
if (!drm) {
2018-07-09 21:49:54 +00:00
wlr_log_errno(WLR_ERROR, "Allocation failed");
2017-05-07 14:00:23 +00:00
return NULL;
}
2017-09-30 09:22:26 +00:00
wlr_backend_init(&drm->backend, &backend_impl);
2017-09-30 09:22:26 +00:00
drm->session = session;
wl_list_init(&drm->fbs);
wl_list_init(&drm->connectors);
2017-05-02 02:34:33 +00:00
drm->dev = dev;
drm->fd = dev->fd;
drm->name = name;
2018-09-17 20:25:20 +00:00
if (parent != NULL) {
drm->parent = get_drm_backend_from_backend(parent);
drm->parent_destroy.notify = handle_parent_destroy;
wl_signal_add(&parent->events.destroy, &drm->parent_destroy);
} else {
wl_list_init(&drm->parent_destroy.link);
2018-09-17 20:25:20 +00:00
}
2017-05-01 05:49:18 +00:00
drm->dev_change.notify = handle_dev_change;
wl_signal_add(&dev->events.change, &drm->dev_change);
2017-06-04 05:43:34 +00:00
drm->dev_remove.notify = handle_dev_remove;
wl_signal_add(&dev->events.remove, &drm->dev_remove);
2017-09-30 09:22:26 +00:00
drm->display = display;
2017-05-03 09:28:44 +00:00
struct wl_event_loop *event_loop = wl_display_get_event_loop(display);
2017-09-30 09:22:26 +00:00
drm->drm_event = wl_event_loop_add_fd(event_loop, drm->fd,
WL_EVENT_READABLE, handle_drm_event, drm);
2017-09-30 09:22:26 +00:00
if (!drm->drm_event) {
2018-07-09 21:49:54 +00:00
wlr_log(WLR_ERROR, "Failed to create DRM event source");
2017-05-01 05:49:18 +00:00
goto error_fd;
}
drm->session_active.notify = handle_session_active;
wl_signal_add(&session->events.active, &drm->session_active);
2017-05-13 13:12:47 +00:00
if (!check_drm_features(drm)) {
2017-08-05 06:15:39 +00:00
goto error_event;
2017-07-20 08:51:59 +00:00
}
if (!init_drm_resources(drm)) {
2017-08-05 06:15:39 +00:00
goto error_event;
}
2017-07-20 08:51:59 +00:00
if (drm->parent) {
if (!init_drm_renderer(drm, &drm->mgpu_renderer)) {
wlr_log(WLR_ERROR, "Failed to initialize renderer");
goto error_resources;
}
// We'll perform a multi-GPU copy for all submitted buffers, we need
// to be able to texture from them
struct wlr_renderer *renderer = drm->mgpu_renderer.wlr_rend;
const struct wlr_drm_format_set *texture_formats =
wlr_renderer_get_dmabuf_texture_formats(renderer);
if (texture_formats == NULL) {
wlr_log(WLR_ERROR, "Failed to query renderer texture formats");
goto error_mgpu_renderer;
}
// Forbid implicit modifiers, because their meaning changes from one
// GPU to another.
for (size_t i = 0; i < texture_formats->len; i++) {
const struct wlr_drm_format *fmt = texture_formats->formats[i];
for (size_t j = 0; j < fmt->len; j++) {
uint64_t mod = fmt->modifiers[j];
if (mod == DRM_FORMAT_MOD_INVALID) {
continue;
}
wlr_drm_format_set_add(&drm->mgpu_formats, fmt->format, mod);
}
}
}
backend/drm, backend/libinput: listen to session destroy This fixes a heap-use-after-free when the session is destroyed before the backend during wl_display_destroy: ==1085==ERROR: AddressSanitizer: heap-use-after-free on address 0x614000000180 at pc 0x7f88e3590c2d bp 0x7ffdc4e33f90 sp 0x7ffdc4e33f80 READ of size 8 at 0x614000000180 thread T0 #0 0x7f88e3590c2c in find_device ../subprojects/wlroots/backend/session/session.c:192 #1 0x7f88e3590e85 in wlr_session_close_file ../subprojects/wlroots/backend/session/session.c:204 #2 0x7f88e357b80c in libinput_close_restricted ../subprojects/wlroots/backend/libinput/backend.c:24 #3 0x7f88e21af274 (/lib64/libinput.so.10+0x28274) #4 0x7f88e21aff1d (/lib64/libinput.so.10+0x28f1d) #5 0x7f88e219ddac (/lib64/libinput.so.10+0x16dac) #6 0x7f88e21b415d in libinput_unref (/lib64/libinput.so.10+0x2d15d) #7 0x7f88e357c9d6 in backend_destroy ../subprojects/wlroots/backend/libinput/backend.c:130 #8 0x7f88e3545a09 in wlr_backend_destroy ../subprojects/wlroots/backend/backend.c:50 #9 0x7f88e358981a in multi_backend_destroy ../subprojects/wlroots/backend/multi/backend.c:54 #10 0x7f88e358a059 in handle_display_destroy ../subprojects/wlroots/backend/multi/backend.c:107 #11 0x7f88e314acde (/lib64/libwayland-server.so.0+0x8cde) #12 0x7f88e314b466 in wl_display_destroy (/lib64/libwayland-server.so.0+0x9466) #13 0x559fefb52385 in main ../main.c:67 #14 0x7f88e2639152 in __libc_start_main (/lib64/libc.so.6+0x27152) #15 0x559fefb4297d in _start (/home/simon/src/glider/build/glider+0x2297d) 0x614000000180 is located 320 bytes inside of 416-byte region [0x614000000040,0x6140000001e0) freed by thread T0 here: #0 0x7f88e3d0a6b0 in __interceptor_free /build/gcc/src/gcc/libsanitizer/asan/asan_malloc_linux.cc:122 #1 0x7f88e35b51fb in logind_session_destroy ../subprojects/wlroots/backend/session/logind.c:270 #2 0x7f88e35905a4 in wlr_session_destroy ../subprojects/wlroots/backend/session/session.c:156 #3 0x7f88e358f440 in handle_display_destroy ../subprojects/wlroots/backend/session/session.c:65 #4 0x7f88e314acde (/lib64/libwayland-server.so.0+0x8cde) previously allocated by thread T0 here: #0 0x7f88e3d0acd8 in __interceptor_calloc /build/gcc/src/gcc/libsanitizer/asan/asan_malloc_linux.cc:153 #1 0x7f88e35b911c in logind_session_create ../subprojects/wlroots/backend/session/logind.c:746 #2 0x7f88e358f6b4 in wlr_session_create ../subprojects/wlroots/backend/session/session.c:91 #3 0x559fefb51ea6 in main ../main.c:20 #4 0x7f88e2639152 in __libc_start_main (/lib64/libc.so.6+0x27152)
2019-11-30 10:57:37 +00:00
drm->session_destroy.notify = handle_session_destroy;
wl_signal_add(&session->events.destroy, &drm->session_destroy);
2017-12-07 22:44:59 +00:00
drm->display_destroy.notify = handle_display_destroy;
wl_display_add_destroy_listener(display, &drm->display_destroy);
2017-09-30 09:22:26 +00:00
return &drm->backend;
2017-05-01 05:49:18 +00:00
error_mgpu_renderer:
finish_drm_renderer(&drm->mgpu_renderer);
error_resources:
finish_drm_resources(drm);
2017-05-03 09:28:44 +00:00
error_event:
wl_list_remove(&drm->session_active.link);
2017-09-30 09:22:26 +00:00
wl_event_source_remove(drm->drm_event);
2017-05-01 05:49:18 +00:00
error_fd:
wl_list_remove(&drm->dev_remove.link);
wl_list_remove(&drm->dev_change.link);
wl_list_remove(&drm->parent_destroy.link);
wlr_session_close_file(drm->session, dev);
2017-09-30 09:22:26 +00:00
free(drm);
2017-05-01 05:49:18 +00:00
return NULL;
}