ui/gtk: refresh rate fixes.
ui/vnc: add support for desktop resize and power contol. ui/vnc: misc bugfixes. -----BEGIN PGP SIGNATURE----- iQIzBAABCgAdFiEEoDKM/7k6F6eZAf59TLbY7tPocTgFAmABbUoACgkQTLbY7tPo cTh2Hg//S+fQH9Lev/J17mFbwiu9PIY+D8bqAFBfEUvRG9I0xvou7L0gYMICdq9o lBwHpdqgppWaHlUAJ8lsf+JiPUQmiZkByuByuwH/Ao4h6FS28MuXTHLJVd1KRbAW Ouzxim+qNfP46jPZKy0xe7dZETAoGe5YQ0sCt3ru5F9dLE4V72bgxPzc1OTbWuiO Xt21/0y4qnesvw6qUB9nCnLJUkozgSoj5+WVYht2dZUttLqJGG7lV61LDZWEsnkW 78pWxgk90r8ZGiy4eqpenVAX5JogmZLpAfXkhsw7kYGIgC6nRI5oXky89iAO5x62 RBIDxWfmqngIM88csMScVU9eukz0kALWXipotaF9ntVzvi6KGYS7Nx1HpIH2r319 DYyZaD3PBafD0FumKOKHWrRrShE5q3NAX5dbRNS3RbG7cX6ZE5wHMdD9nHm0AYIX 3Ljv5DIfw+MC+ivXopRYiMigJQkjyPXWpeUVw7m14G986YyKCaJJDRSb1OD1Vc2Q BUP6uWlajj1y4ODz+GN61P8dE/5k8/tvn/3xlWvUqrWeA8NETy1z4i1F2V4mU9xM +42ifSUZFwDmcZ2Y3GqLfpjcZTXcOLL1dGjDRJbL9p9RKFDtpG5afOsCWg+fG0zM ni0qx0QS7AoJNQduvy9HVhFxT2AaYSKSMYfQN6fRZxBtZashAKQ= =IgLz -----END PGP SIGNATURE----- Merge remote-tracking branch 'remotes/kraxel/tags/ui-20210115-pull-request' into staging ui/gtk: refresh rate fixes. ui/vnc: add support for desktop resize and power contol. ui/vnc: misc bugfixes. # gpg: Signature made Fri 15 Jan 2021 10:24:10 GMT # gpg: using RSA key A0328CFFB93A17A79901FE7D4CB6D8EED3E87138 # gpg: Good signature from "Gerd Hoffmann (work) <kraxel@redhat.com>" [full] # gpg: aka "Gerd Hoffmann <gerd@kraxel.org>" [full] # gpg: aka "Gerd Hoffmann (private) <kraxel@gmail.com>" [full] # Primary key fingerprint: A032 8CFF B93A 17A7 9901 FE7D 4CB6 D8EE D3E8 7138 * remotes/kraxel/tags/ui-20210115-pull-request: vnc: add support for extended desktop resize vnc: move initialization to framebuffer_update_request vnc: move check into vnc_cursor_define vnc: Fix a memleak in vnc_display_connect() ui: add support for remote power control to VNC server vnc: fix unfinalized tlscreds for VncDisplay ui/gtk: update monitor interval on egl displays ui/gtk: expose gd_monitor_update_interval ui/gtk: limit virtual console max update interval ui/gtk: rename variable window to widget ui/gtk: don't try to redefine SI prefixes Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
This commit is contained in:
commit
7cb6b97300
@ -24,8 +24,6 @@
|
||||
#include "ui/egl-context.h"
|
||||
#endif
|
||||
|
||||
#define MILLISEC_PER_SEC 1000000
|
||||
|
||||
typedef struct GtkDisplayState GtkDisplayState;
|
||||
|
||||
typedef struct VirtualGfxConsole {
|
||||
@ -88,6 +86,7 @@ extern bool gtk_use_gl_area;
|
||||
|
||||
/* ui/gtk.c */
|
||||
void gd_update_windowsize(VirtualConsole *vc);
|
||||
int gd_monitor_update_interval(GtkWidget *widget);
|
||||
|
||||
/* ui/gtk-egl.c */
|
||||
void gd_egl_init(VirtualConsole *vc);
|
||||
|
@ -2222,6 +2222,10 @@ SRST
|
||||
transmission. When not using an -audiodev argument, this option
|
||||
must be omitted, otherwise is must be present and specify a
|
||||
valid audiodev.
|
||||
|
||||
``power-control``
|
||||
Permit the remote client to issue shutdown, reboot or reset power
|
||||
control requests.
|
||||
ERST
|
||||
|
||||
ARCHHEADING(, QEMU_ARCH_I386)
|
||||
|
@ -113,6 +113,9 @@ void gd_egl_refresh(DisplayChangeListener *dcl)
|
||||
{
|
||||
VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl);
|
||||
|
||||
vc->gfx.dcl.update_interval = gd_monitor_update_interval(
|
||||
vc->window ? vc->window : vc->gfx.drawing_area);
|
||||
|
||||
if (!vc->gfx.esurface) {
|
||||
gd_egl_init(vc);
|
||||
if (!vc->gfx.esurface) {
|
||||
|
25
ui/gtk.c
25
ui/gtk.c
@ -749,19 +749,24 @@ static void gd_resize_event(GtkGLArea *area,
|
||||
#endif
|
||||
|
||||
/*
|
||||
* If available, return the refresh rate of the display in milli-Hertz,
|
||||
* else return 0.
|
||||
* If available, return the update interval of the monitor in ms,
|
||||
* else return 0 (the default update interval).
|
||||
*/
|
||||
static int gd_refresh_rate_millihz(GtkWidget *window)
|
||||
int gd_monitor_update_interval(GtkWidget *widget)
|
||||
{
|
||||
#ifdef GDK_VERSION_3_22
|
||||
GdkWindow *win = gtk_widget_get_window(window);
|
||||
GdkWindow *win = gtk_widget_get_window(widget);
|
||||
|
||||
if (win) {
|
||||
GdkDisplay *dpy = gtk_widget_get_display(window);
|
||||
GdkDisplay *dpy = gtk_widget_get_display(widget);
|
||||
GdkMonitor *monitor = gdk_display_get_monitor_at_window(dpy, win);
|
||||
int refresh_rate = gdk_monitor_get_refresh_rate(monitor); /* [mHz] */
|
||||
|
||||
return gdk_monitor_get_refresh_rate(monitor);
|
||||
if (refresh_rate) {
|
||||
/* T = 1 / f = 1 [s*Hz] / f = 1000*1000 [ms*mHz] / f */
|
||||
return MIN(1000 * 1000 / refresh_rate,
|
||||
GUI_REFRESH_INTERVAL_DEFAULT);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
@ -774,7 +779,6 @@ static gboolean gd_draw_event(GtkWidget *widget, cairo_t *cr, void *opaque)
|
||||
int mx, my;
|
||||
int ww, wh;
|
||||
int fbw, fbh;
|
||||
int refresh_rate_millihz;
|
||||
|
||||
#if defined(CONFIG_OPENGL)
|
||||
if (vc->gfx.gls) {
|
||||
@ -795,11 +799,8 @@ static gboolean gd_draw_event(GtkWidget *widget, cairo_t *cr, void *opaque)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
refresh_rate_millihz = gd_refresh_rate_millihz(vc->window ?
|
||||
vc->window : s->window);
|
||||
if (refresh_rate_millihz) {
|
||||
vc->gfx.dcl.update_interval = MILLISEC_PER_SEC / refresh_rate_millihz;
|
||||
}
|
||||
vc->gfx.dcl.update_interval =
|
||||
gd_monitor_update_interval(vc->window ? vc->window : s->window);
|
||||
|
||||
fbw = surface_width(vc->gfx.ds);
|
||||
fbh = surface_height(vc->gfx.ds);
|
||||
|
147
ui/vnc.c
147
ui/vnc.c
@ -30,6 +30,7 @@
|
||||
#include "trace.h"
|
||||
#include "hw/qdev-core.h"
|
||||
#include "sysemu/sysemu.h"
|
||||
#include "sysemu/runstate.h"
|
||||
#include "qemu/error-report.h"
|
||||
#include "qemu/main-loop.h"
|
||||
#include "qemu/module.h"
|
||||
@ -654,14 +655,35 @@ void vnc_framebuffer_update(VncState *vs, int x, int y, int w, int h,
|
||||
vnc_write_s32(vs, encoding);
|
||||
}
|
||||
|
||||
static void vnc_desktop_resize_ext(VncState *vs, int reject_reason)
|
||||
{
|
||||
vnc_lock_output(vs);
|
||||
vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE);
|
||||
vnc_write_u8(vs, 0);
|
||||
vnc_write_u16(vs, 1); /* number of rects */
|
||||
vnc_framebuffer_update(vs,
|
||||
reject_reason ? 1 : 0,
|
||||
reject_reason,
|
||||
vs->client_width, vs->client_height,
|
||||
VNC_ENCODING_DESKTOP_RESIZE_EXT);
|
||||
vnc_write_u8(vs, 1); /* number of screens */
|
||||
vnc_write_u8(vs, 0); /* padding */
|
||||
vnc_write_u8(vs, 0); /* padding */
|
||||
vnc_write_u8(vs, 0); /* padding */
|
||||
vnc_write_u32(vs, 0); /* screen id */
|
||||
vnc_write_u16(vs, 0); /* screen x-pos */
|
||||
vnc_write_u16(vs, 0); /* screen y-pos */
|
||||
vnc_write_u16(vs, vs->client_width);
|
||||
vnc_write_u16(vs, vs->client_height);
|
||||
vnc_write_u32(vs, 0); /* screen flags */
|
||||
vnc_unlock_output(vs);
|
||||
vnc_flush(vs);
|
||||
}
|
||||
|
||||
static void vnc_desktop_resize(VncState *vs)
|
||||
{
|
||||
if (vs->ioc == NULL || !vnc_has_feature(vs, VNC_FEATURE_RESIZE)) {
|
||||
return;
|
||||
}
|
||||
if (vs->client_width == pixman_image_get_width(vs->vd->server) &&
|
||||
vs->client_height == pixman_image_get_height(vs->vd->server)) {
|
||||
if (vs->ioc == NULL || (!vnc_has_feature(vs, VNC_FEATURE_RESIZE) &&
|
||||
!vnc_has_feature(vs, VNC_FEATURE_RESIZE_EXT))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -671,6 +693,12 @@ static void vnc_desktop_resize(VncState *vs)
|
||||
pixman_image_get_height(vs->vd->server) >= 0);
|
||||
vs->client_width = pixman_image_get_width(vs->vd->server);
|
||||
vs->client_height = pixman_image_get_height(vs->vd->server);
|
||||
|
||||
if (vnc_has_feature(vs, VNC_FEATURE_RESIZE_EXT)) {
|
||||
vnc_desktop_resize_ext(vs, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
vnc_lock_output(vs);
|
||||
vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE);
|
||||
vnc_write_u8(vs, 0);
|
||||
@ -792,9 +820,7 @@ static void vnc_dpy_switch(DisplayChangeListener *dcl,
|
||||
QTAILQ_FOREACH(vs, &vd->clients, next) {
|
||||
vnc_colordepth(vs);
|
||||
vnc_desktop_resize(vs);
|
||||
if (vs->vd->cursor) {
|
||||
vnc_cursor_define(vs);
|
||||
}
|
||||
vnc_cursor_define(vs);
|
||||
memset(vs->dirty, 0x00, sizeof(vs->dirty));
|
||||
vnc_set_area_dirty(vs->dirty, vd, 0, 0,
|
||||
vnc_width(vd),
|
||||
@ -928,6 +954,10 @@ static int vnc_cursor_define(VncState *vs)
|
||||
QEMUCursor *c = vs->vd->cursor;
|
||||
int isize;
|
||||
|
||||
if (!vs->vd->cursor) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (vnc_has_feature(vs, VNC_FEATURE_ALPHA_CURSOR)) {
|
||||
vnc_lock_output(vs);
|
||||
vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE);
|
||||
@ -2011,6 +2041,10 @@ static void framebuffer_update_request(VncState *vs, int incremental,
|
||||
} else {
|
||||
vs->update = VNC_STATE_UPDATE_FORCE;
|
||||
vnc_set_area_dirty(vs->dirty, vs->vd, x, y, w, h);
|
||||
vnc_colordepth(vs);
|
||||
vnc_desktop_resize(vs);
|
||||
vnc_led_state_change(vs);
|
||||
vnc_cursor_define(vs);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2042,6 +2076,17 @@ static void send_ext_audio_ack(VncState *vs)
|
||||
vnc_flush(vs);
|
||||
}
|
||||
|
||||
static void send_xvp_message(VncState *vs, int code)
|
||||
{
|
||||
vnc_lock_output(vs);
|
||||
vnc_write_u8(vs, VNC_MSG_SERVER_XVP);
|
||||
vnc_write_u8(vs, 0); /* pad */
|
||||
vnc_write_u8(vs, 1); /* version */
|
||||
vnc_write_u8(vs, code);
|
||||
vnc_unlock_output(vs);
|
||||
vnc_flush(vs);
|
||||
}
|
||||
|
||||
static void set_encodings(VncState *vs, int32_t *encodings, size_t n_encodings)
|
||||
{
|
||||
int i;
|
||||
@ -2100,6 +2145,9 @@ static void set_encodings(VncState *vs, int32_t *encodings, size_t n_encodings)
|
||||
case VNC_ENCODING_DESKTOPRESIZE:
|
||||
vs->features |= VNC_FEATURE_RESIZE_MASK;
|
||||
break;
|
||||
case VNC_ENCODING_DESKTOP_RESIZE_EXT:
|
||||
vs->features |= VNC_FEATURE_RESIZE_EXT_MASK;
|
||||
break;
|
||||
case VNC_ENCODING_POINTER_TYPE_CHANGE:
|
||||
vs->features |= VNC_FEATURE_POINTER_TYPE_CHANGE_MASK;
|
||||
break;
|
||||
@ -2121,6 +2169,12 @@ static void set_encodings(VncState *vs, int32_t *encodings, size_t n_encodings)
|
||||
case VNC_ENCODING_LED_STATE:
|
||||
vs->features |= VNC_FEATURE_LED_STATE_MASK;
|
||||
break;
|
||||
case VNC_ENCODING_XVP:
|
||||
if (vs->vd->power_control) {
|
||||
vs->features |= VNC_FEATURE_XVP;
|
||||
send_xvp_message(vs, VNC_XVP_CODE_INIT);
|
||||
}
|
||||
break;
|
||||
case VNC_ENCODING_COMPRESSLEVEL0 ... VNC_ENCODING_COMPRESSLEVEL0 + 9:
|
||||
vs->tight->compression = (enc & 0x0F);
|
||||
break;
|
||||
@ -2134,12 +2188,7 @@ static void set_encodings(VncState *vs, int32_t *encodings, size_t n_encodings)
|
||||
break;
|
||||
}
|
||||
}
|
||||
vnc_desktop_resize(vs);
|
||||
check_pointer_type_change(&vs->mouse_mode_notifier, NULL);
|
||||
vnc_led_state_change(vs);
|
||||
if (vs->vd->cursor) {
|
||||
vnc_cursor_define(vs);
|
||||
}
|
||||
}
|
||||
|
||||
static void set_pixel_conversion(VncState *vs)
|
||||
@ -2353,6 +2402,42 @@ static int protocol_client_msg(VncState *vs, uint8_t *data, size_t len)
|
||||
|
||||
client_cut_text(vs, read_u32(data, 4), data + 8);
|
||||
break;
|
||||
case VNC_MSG_CLIENT_XVP:
|
||||
if (!(vs->features & VNC_FEATURE_XVP)) {
|
||||
error_report("vnc: xvp client message while disabled");
|
||||
vnc_client_error(vs);
|
||||
break;
|
||||
}
|
||||
if (len == 1) {
|
||||
return 4;
|
||||
}
|
||||
if (len == 4) {
|
||||
uint8_t version = read_u8(data, 2);
|
||||
uint8_t action = read_u8(data, 3);
|
||||
|
||||
if (version != 1) {
|
||||
error_report("vnc: xvp client message version %d != 1",
|
||||
version);
|
||||
vnc_client_error(vs);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case VNC_XVP_ACTION_SHUTDOWN:
|
||||
qemu_system_powerdown_request();
|
||||
break;
|
||||
case VNC_XVP_ACTION_REBOOT:
|
||||
send_xvp_message(vs, VNC_XVP_CODE_FAIL);
|
||||
break;
|
||||
case VNC_XVP_ACTION_RESET:
|
||||
qemu_system_reset_request(SHUTDOWN_CAUSE_HOST_QMP_SYSTEM_RESET);
|
||||
break;
|
||||
default:
|
||||
send_xvp_message(vs, VNC_XVP_CODE_FAIL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case VNC_MSG_CLIENT_QEMU:
|
||||
if (len == 1)
|
||||
return 2;
|
||||
@ -2423,6 +2508,34 @@ static int protocol_client_msg(VncState *vs, uint8_t *data, size_t len)
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case VNC_MSG_CLIENT_SET_DESKTOP_SIZE:
|
||||
{
|
||||
size_t size;
|
||||
uint8_t screens;
|
||||
|
||||
if (len < 8) {
|
||||
return 8;
|
||||
}
|
||||
|
||||
screens = read_u8(data, 6);
|
||||
size = 8 + screens * 16;
|
||||
if (len < size) {
|
||||
return size;
|
||||
}
|
||||
|
||||
if (dpy_ui_info_supported(vs->vd->dcl.con)) {
|
||||
QemuUIInfo info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
info.width = read_u16(data, 2);
|
||||
info.height = read_u16(data, 4);
|
||||
dpy_set_ui_info(vs->vd->dcl.con, &info);
|
||||
vnc_desktop_resize_ext(vs, 4 /* Request forwarded */);
|
||||
} else {
|
||||
vnc_desktop_resize_ext(vs, 3 /* Invalid screen layout */);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
VNC_DEBUG("Msg: %d\n", data[0]);
|
||||
vnc_client_error(vs);
|
||||
@ -3234,7 +3347,7 @@ static void vnc_display_close(VncDisplay *vd)
|
||||
vd->auth = VNC_AUTH_INVALID;
|
||||
vd->subauth = VNC_AUTH_INVALID;
|
||||
if (vd->tlscreds) {
|
||||
object_unparent(OBJECT(vd->tlscreds));
|
||||
object_unref(OBJECT(vd->tlscreds));
|
||||
vd->tlscreds = NULL;
|
||||
}
|
||||
if (vd->tlsauthz) {
|
||||
@ -3379,6 +3492,9 @@ static QemuOptsList qemu_vnc_opts = {
|
||||
},{
|
||||
.name = "audiodev",
|
||||
.type = QEMU_OPT_STRING,
|
||||
},{
|
||||
.name = "power-control",
|
||||
.type = QEMU_OPT_BOOL,
|
||||
},
|
||||
{ /* end of list */ }
|
||||
},
|
||||
@ -3748,6 +3864,7 @@ static int vnc_display_connect(VncDisplay *vd,
|
||||
sioc = qio_channel_socket_new();
|
||||
qio_channel_set_name(QIO_CHANNEL(sioc), "vnc-reverse");
|
||||
if (qio_channel_socket_connect_sync(sioc, saddr[0], errp) < 0) {
|
||||
object_unref(OBJECT(sioc));
|
||||
return -1;
|
||||
}
|
||||
vnc_connect(vd, sioc, false, false);
|
||||
@ -3942,6 +4059,8 @@ void vnc_display_open(const char *id, Error **errp)
|
||||
vd->non_adaptive = true;
|
||||
}
|
||||
|
||||
vd->power_control = qemu_opt_get_bool(opts, "power-control", false);
|
||||
|
||||
if (tlsauthz) {
|
||||
vd->tlsauthzid = g_strdup(tlsauthz);
|
||||
} else if (acl) {
|
||||
|
15
ui/vnc.h
15
ui/vnc.h
@ -176,6 +176,7 @@ struct VncDisplay
|
||||
int ws_subauth; /* Used by websockets */
|
||||
bool lossy;
|
||||
bool non_adaptive;
|
||||
bool power_control;
|
||||
QCryptoTLSCreds *tlscreds;
|
||||
QAuthZ *tlsauthz;
|
||||
char *tlsauthzid;
|
||||
@ -412,6 +413,7 @@ enum {
|
||||
#define VNC_ENCODING_TIGHT_PNG 0xFFFFFEFC /* -260 */
|
||||
#define VNC_ENCODING_LED_STATE 0XFFFFFEFB /* -261 */
|
||||
#define VNC_ENCODING_DESKTOP_RESIZE_EXT 0XFFFFFECC /* -308 */
|
||||
#define VNC_ENCODING_XVP 0XFFFFFECB /* -309 */
|
||||
#define VNC_ENCODING_ALPHA_CURSOR 0XFFFFFEC6 /* -314 */
|
||||
#define VNC_ENCODING_WMVi 0x574D5669
|
||||
|
||||
@ -442,6 +444,7 @@ enum {
|
||||
|
||||
enum VncFeatures {
|
||||
VNC_FEATURE_RESIZE,
|
||||
VNC_FEATURE_RESIZE_EXT,
|
||||
VNC_FEATURE_HEXTILE,
|
||||
VNC_FEATURE_POINTER_TYPE_CHANGE,
|
||||
VNC_FEATURE_WMVI,
|
||||
@ -453,9 +456,11 @@ enum VncFeatures {
|
||||
VNC_FEATURE_ZRLE,
|
||||
VNC_FEATURE_ZYWRLE,
|
||||
VNC_FEATURE_LED_STATE,
|
||||
VNC_FEATURE_XVP,
|
||||
};
|
||||
|
||||
#define VNC_FEATURE_RESIZE_MASK (1 << VNC_FEATURE_RESIZE)
|
||||
#define VNC_FEATURE_RESIZE_EXT_MASK (1 << VNC_FEATURE_RESIZE_EXT)
|
||||
#define VNC_FEATURE_HEXTILE_MASK (1 << VNC_FEATURE_HEXTILE)
|
||||
#define VNC_FEATURE_POINTER_TYPE_CHANGE_MASK (1 << VNC_FEATURE_POINTER_TYPE_CHANGE)
|
||||
#define VNC_FEATURE_WMVI_MASK (1 << VNC_FEATURE_WMVI)
|
||||
@ -467,6 +472,7 @@ enum VncFeatures {
|
||||
#define VNC_FEATURE_ZRLE_MASK (1 << VNC_FEATURE_ZRLE)
|
||||
#define VNC_FEATURE_ZYWRLE_MASK (1 << VNC_FEATURE_ZYWRLE)
|
||||
#define VNC_FEATURE_LED_STATE_MASK (1 << VNC_FEATURE_LED_STATE)
|
||||
#define VNC_FEATURE_XVP_MASK (1 << VNC_FEATURE_XVP)
|
||||
|
||||
|
||||
/* Client -> Server message IDs */
|
||||
@ -519,6 +525,15 @@ enum VncFeatures {
|
||||
#define VNC_MSG_SERVER_QEMU_AUDIO_BEGIN 1
|
||||
#define VNC_MSG_SERVER_QEMU_AUDIO_DATA 2
|
||||
|
||||
/* XVP server -> client status code */
|
||||
#define VNC_XVP_CODE_FAIL 0
|
||||
#define VNC_XVP_CODE_INIT 1
|
||||
|
||||
/* XVP client -> server action request */
|
||||
#define VNC_XVP_ACTION_SHUTDOWN 2
|
||||
#define VNC_XVP_ACTION_REBOOT 3
|
||||
#define VNC_XVP_ACTION_RESET 4
|
||||
|
||||
|
||||
/*****************************************************************************
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user