Remove qemu dependency.
Remove obsolete pieces from libardware_legacy that were related to QEMU (i.e. running the system image under emulation) but are no longer used. Note that qemu tracing isn't implemented by the emulator anymore (and has been for a long time). BUG=25875346 Change-Id: I2593fe9f90241af9c5076fa248c30e0d8ab5d663
This commit is contained in:
parent
725758d8c1
commit
14ca884e01
7 changed files with 1 additions and 641 deletions
|
@ -1,7 +1,7 @@
|
|||
# Copyright 2006 The Android Open Source Project
|
||||
|
||||
# Setting LOCAL_PATH will mess up all-subdir-makefiles, so do it beforehand.
|
||||
legacy_modules := power uevent wifi qemu qemu_tracing
|
||||
legacy_modules := power uevent wifi
|
||||
|
||||
SAVE_MAKEFILES := $(call all-named-subdir-makefiles,$(legacy_modules))
|
||||
LEGACY_AUDIO_MAKEFILES := $(call all-named-subdir-makefiles,audio)
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef _HARDWARE_QEMU_TRACING_H
|
||||
#define _HARDWARE_QEMU_TRACING_H
|
||||
|
||||
#if __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int qemu_start_tracing();
|
||||
int qemu_stop_tracing();
|
||||
int qemu_add_mapping(unsigned int addr, const char *name);
|
||||
int qemu_remove_mapping(unsigned int addr);
|
||||
|
||||
#if __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif // _HARDWARE_QEMU_TRACING_H
|
112
qemu.h
112
qemu.h
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
#ifndef _libs_hardware_qemu_h
|
||||
#define _libs_hardware_qemu_h
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifdef QEMU_HARDWARE
|
||||
|
||||
/* returns 1 iff we're running in the emulator */
|
||||
extern int qemu_check(void);
|
||||
|
||||
/* a structure used to hold enough state to connect to a given
|
||||
* QEMU communication channel, either through a qemud socket or
|
||||
* a serial port.
|
||||
*
|
||||
* initialize the structure by zero-ing it out
|
||||
*/
|
||||
typedef struct {
|
||||
char is_inited;
|
||||
char is_available;
|
||||
char is_qemud;
|
||||
char is_qemud_old;
|
||||
char is_tty;
|
||||
int fd;
|
||||
char device[32];
|
||||
} QemuChannel;
|
||||
|
||||
/* try to open a qemu communication channel.
|
||||
* returns a file descriptor on success, or -1 in case of
|
||||
* error.
|
||||
*
|
||||
* 'channel' must be a QemuChannel structure that is empty
|
||||
* on the first call. You can call this function several
|
||||
* time to re-open the channel using the same 'channel'
|
||||
* object to speed things a bit.
|
||||
*/
|
||||
extern int qemu_channel_open( QemuChannel* channel,
|
||||
const char* name,
|
||||
int mode );
|
||||
|
||||
/* create a command made of a 4-hexchar prefix followed
|
||||
* by the content. the prefix contains the content's length
|
||||
* in hexadecimal coding.
|
||||
*
|
||||
* 'buffer' must be at last 6 bytes
|
||||
* returns -1 in case of overflow, or the command's total length
|
||||
* otherwise (i.e. content length + 4)
|
||||
*/
|
||||
extern int qemu_command_format( char* buffer,
|
||||
int buffer_size,
|
||||
const char* format,
|
||||
... );
|
||||
|
||||
/* directly sends a command through the 'hw-control' channel.
|
||||
* this will open the channel, send the formatted command, then
|
||||
* close the channel automatically.
|
||||
* returns 0 on success, or -1 on error.
|
||||
*/
|
||||
extern int qemu_control_command( const char* fmt, ... );
|
||||
|
||||
/* sends a question to the hw-control channel, then receive an answer in
|
||||
* a user-allocated buffer. returns the length of the answer, or -1
|
||||
* in case of error.
|
||||
*
|
||||
* 'question' *must* have been formatted through qemu_command_format
|
||||
*/
|
||||
extern int qemu_control_query( const char* question, int questionlen,
|
||||
char* answer, int answersize );
|
||||
|
||||
#endif /* QEMU_HARDWARE */
|
||||
|
||||
/* use QEMU_FALLBACK(call) to call a QEMU-specific callback */
|
||||
/* use QEMU_FALLBACK_VOID(call) if the function returns void */
|
||||
#ifdef QEMU_HARDWARE
|
||||
# define QEMU_FALLBACK(x) \
|
||||
do { \
|
||||
if (qemu_check()) \
|
||||
return qemu_ ## x ; \
|
||||
} while (0)
|
||||
# define QEMU_FALLBACK_VOID(x) \
|
||||
do { \
|
||||
if (qemu_check()) { \
|
||||
qemu_ ## x ; \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
#else
|
||||
# define QEMU_FALLBACK(x) ((void)0)
|
||||
# define QEMU_FALLBACK_VOID(x) ((void)0)
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _libs_hardware_qemu_h */
|
|
@ -1,3 +0,0 @@
|
|||
ifeq ($(QEMU_HARDWARE),true)
|
||||
LOCAL_SRC_FILES += qemu/qemu.c
|
||||
endif
|
402
qemu/qemu.c
402
qemu/qemu.c
|
@ -1,402 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* this file contains various functions used by all libhardware modules
|
||||
* that support QEMU emulation
|
||||
*/
|
||||
#include "qemu.h"
|
||||
#define LOG_TAG "hardware-qemu"
|
||||
#include <cutils/log.h>
|
||||
#include <cutils/properties.h>
|
||||
#include <cutils/sockets.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <termios.h>
|
||||
|
||||
#define QEMU_DEBUG 0
|
||||
|
||||
#if QEMU_DEBUG
|
||||
# define D(...) ALOGD(__VA_ARGS__)
|
||||
#else
|
||||
# define D(...) ((void)0)
|
||||
#endif
|
||||
|
||||
#define QEMU_PIPE_DEBUG(...) D(__VA_ARGS__)
|
||||
#include <system/qemu_pipe.h>
|
||||
|
||||
int
|
||||
qemu_check(void)
|
||||
{
|
||||
static int in_qemu = -1;
|
||||
|
||||
if (__builtin_expect(in_qemu < 0,0)) {
|
||||
char propBuf[PROPERTY_VALUE_MAX];
|
||||
property_get("ro.kernel.qemu", propBuf, "");
|
||||
in_qemu = (propBuf[0] == '1');
|
||||
}
|
||||
return in_qemu;
|
||||
}
|
||||
|
||||
static int
|
||||
qemu_fd_write( int fd, const char* cmd, int len )
|
||||
{
|
||||
int len2;
|
||||
do {
|
||||
len2 = write(fd, cmd, len);
|
||||
} while (len2 < 0 && errno == EINTR);
|
||||
return len2;
|
||||
}
|
||||
|
||||
static int
|
||||
qemu_fd_read( int fd, char* buff, int len )
|
||||
{
|
||||
int len2;
|
||||
do {
|
||||
len2 = read(fd, buff, len);
|
||||
} while (len2 < 0 && errno == EINTR);
|
||||
return len2;
|
||||
}
|
||||
|
||||
static int
|
||||
qemu_channel_open_qemud_pipe( QemuChannel* channel,
|
||||
const char* name )
|
||||
{
|
||||
int fd;
|
||||
char pipe_name[512];
|
||||
|
||||
snprintf(pipe_name, sizeof(pipe_name), "pipe:qemud:%s", name);
|
||||
fd = qemu_pipe_open(pipe_name);
|
||||
if (fd < 0) {
|
||||
D("no qemud pipe: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
channel->is_qemud = 1;
|
||||
channel->fd = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
qemu_channel_open_qemud( QemuChannel* channel,
|
||||
const char* name )
|
||||
{
|
||||
int fd, ret, namelen = strlen(name);
|
||||
char answer[2];
|
||||
|
||||
fd = socket_local_client( "qemud",
|
||||
ANDROID_SOCKET_NAMESPACE_RESERVED,
|
||||
SOCK_STREAM );
|
||||
if (fd < 0) {
|
||||
D("no qemud control socket: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* send service name to connect */
|
||||
if (qemu_fd_write(fd, name, namelen) != namelen) {
|
||||
D("can't send service name to qemud: %s",
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* read answer from daemon */
|
||||
if (qemu_fd_read(fd, answer, 2) != 2 ||
|
||||
answer[0] != 'O' || answer[1] != 'K') {
|
||||
D("cant' connect to %s service through qemud", name);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
channel->is_qemud = 1;
|
||||
channel->fd = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
qemu_channel_open_qemud_old( QemuChannel* channel,
|
||||
const char* name )
|
||||
{
|
||||
int fd;
|
||||
|
||||
snprintf(channel->device, sizeof channel->device,
|
||||
"qemud_%s", name);
|
||||
|
||||
fd = socket_local_client( channel->device,
|
||||
ANDROID_SOCKET_NAMESPACE_RESERVED,
|
||||
SOCK_STREAM );
|
||||
if (fd < 0) {
|
||||
D("no '%s' control socket available: %s",
|
||||
channel->device, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
channel->is_qemud_old = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
qemu_channel_open_tty( QemuChannel* channel,
|
||||
const char* name,
|
||||
int mode )
|
||||
{
|
||||
char key[PROPERTY_KEY_MAX];
|
||||
char prop[PROPERTY_VALUE_MAX];
|
||||
int ret;
|
||||
|
||||
ret = snprintf(key, sizeof key, "ro.kernel.android.%s", name);
|
||||
if (ret >= (int)sizeof key)
|
||||
return -1;
|
||||
|
||||
if (property_get(key, prop, "") == 0) {
|
||||
D("no kernel-provided %s device name", name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = snprintf(channel->device, sizeof channel->device,
|
||||
"/dev/%s", prop);
|
||||
if (ret >= (int)sizeof channel->device) {
|
||||
D("%s device name too long: '%s'", name, prop);
|
||||
return -1;
|
||||
}
|
||||
|
||||
channel->is_tty = !memcmp("/dev/tty", channel->device, 8);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
qemu_channel_open( QemuChannel* channel,
|
||||
const char* name,
|
||||
int mode )
|
||||
{
|
||||
int fd = -1;
|
||||
|
||||
/* initialize the channel is needed */
|
||||
if (!channel->is_inited)
|
||||
{
|
||||
channel->is_inited = 1;
|
||||
|
||||
do {
|
||||
if (qemu_channel_open_qemud_pipe(channel, name) == 0)
|
||||
break;
|
||||
|
||||
if (qemu_channel_open_qemud(channel, name) == 0)
|
||||
break;
|
||||
|
||||
if (qemu_channel_open_qemud_old(channel, name) == 0)
|
||||
break;
|
||||
|
||||
if (qemu_channel_open_tty(channel, name, mode) == 0)
|
||||
break;
|
||||
|
||||
channel->is_available = 0;
|
||||
return -1;
|
||||
} while (0);
|
||||
|
||||
channel->is_available = 1;
|
||||
}
|
||||
|
||||
/* try to open the file */
|
||||
if (!channel->is_available) {
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (channel->is_qemud) {
|
||||
return dup(channel->fd);
|
||||
}
|
||||
|
||||
if (channel->is_qemud_old) {
|
||||
do {
|
||||
fd = socket_local_client( channel->device,
|
||||
ANDROID_SOCKET_NAMESPACE_RESERVED,
|
||||
SOCK_STREAM );
|
||||
} while (fd < 0 && errno == EINTR);
|
||||
}
|
||||
else /* /dev/ttySn ? */
|
||||
{
|
||||
do {
|
||||
fd = open(channel->device, mode);
|
||||
} while (fd < 0 && errno == EINTR);
|
||||
|
||||
/* disable ECHO on serial lines */
|
||||
if (fd >= 0 && channel->is_tty) {
|
||||
struct termios ios;
|
||||
tcgetattr( fd, &ios );
|
||||
ios.c_lflag = 0; /* disable ECHO, ICANON, etc... */
|
||||
tcsetattr( fd, TCSANOW, &ios );
|
||||
}
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
qemu_command_vformat( char* buffer,
|
||||
int buffer_size,
|
||||
const char* format,
|
||||
va_list args )
|
||||
{
|
||||
char header[5];
|
||||
int len;
|
||||
|
||||
if (buffer_size < 6)
|
||||
return -1;
|
||||
|
||||
len = vsnprintf(buffer+4, buffer_size-4, format, args);
|
||||
if (len >= buffer_size-4)
|
||||
return -1;
|
||||
|
||||
snprintf(header, sizeof header, "%04x", len);
|
||||
memcpy(buffer, header, 4);
|
||||
return len + 4;
|
||||
}
|
||||
|
||||
extern int
|
||||
qemu_command_format( char* buffer,
|
||||
int buffer_size,
|
||||
const char* format,
|
||||
... )
|
||||
{
|
||||
va_list args;
|
||||
int ret;
|
||||
|
||||
va_start(args, format);
|
||||
ret = qemu_command_vformat(buffer, buffer_size, format, args);
|
||||
va_end(args);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
qemu_control_fd(void)
|
||||
{
|
||||
static QemuChannel channel[1];
|
||||
int fd;
|
||||
|
||||
fd = qemu_channel_open( channel, "hw-control", O_RDWR );
|
||||
if (fd < 0) {
|
||||
D("%s: could not open control channel: %s", __FUNCTION__,
|
||||
strerror(errno));
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int
|
||||
qemu_control_send(const char* cmd, int len)
|
||||
{
|
||||
int fd, len2;
|
||||
|
||||
if (len < 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
fd = qemu_control_fd();
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
|
||||
len2 = qemu_fd_write(fd, cmd, len);
|
||||
close(fd);
|
||||
if (len2 != len) {
|
||||
D("%s: could not send everything %d < %d",
|
||||
__FUNCTION__, len2, len);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
qemu_control_command( const char* fmt, ... )
|
||||
{
|
||||
va_list args;
|
||||
char command[256];
|
||||
int len, fd;
|
||||
|
||||
va_start(args, fmt);
|
||||
len = qemu_command_vformat( command, sizeof command, fmt, args );
|
||||
va_end(args);
|
||||
|
||||
if (len < 0 || len >= (int)sizeof command) {
|
||||
if (len < 0) {
|
||||
D("%s: could not send: %s", __FUNCTION__, strerror(errno));
|
||||
} else {
|
||||
D("%s: too large %d > %d", __FUNCTION__, len, (int)(sizeof command));
|
||||
}
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return qemu_control_send( command, len );
|
||||
}
|
||||
|
||||
extern int qemu_control_query( const char* question, int questionlen,
|
||||
char* answer, int answersize )
|
||||
{
|
||||
int ret, fd, len, result = -1;
|
||||
char header[5], *end;
|
||||
|
||||
if (questionlen <= 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
fd = qemu_control_fd();
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
|
||||
ret = qemu_fd_write( fd, question, questionlen );
|
||||
if (ret != questionlen) {
|
||||
D("%s: could not write all: %d < %d", __FUNCTION__,
|
||||
ret, questionlen);
|
||||
goto Exit;
|
||||
}
|
||||
|
||||
/* read a 4-byte header giving the length of the following content */
|
||||
ret = qemu_fd_read( fd, header, 4 );
|
||||
if (ret != 4) {
|
||||
D("%s: could not read header (%d != 4)",
|
||||
__FUNCTION__, ret);
|
||||
goto Exit;
|
||||
}
|
||||
|
||||
header[4] = 0;
|
||||
len = strtol( header, &end, 16 );
|
||||
if ( len < 0 || end == NULL || end != header+4 || len > answersize ) {
|
||||
D("%s: could not parse header: '%s'",
|
||||
__FUNCTION__, header);
|
||||
goto Exit;
|
||||
}
|
||||
|
||||
/* read the answer */
|
||||
ret = qemu_fd_read( fd, answer, len );
|
||||
if (ret != len) {
|
||||
D("%s: could not read all of answer %d < %d",
|
||||
__FUNCTION__, ret, len);
|
||||
goto Exit;
|
||||
}
|
||||
|
||||
result = len;
|
||||
|
||||
Exit:
|
||||
close(fd);
|
||||
return result;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
# Copyright 2007 The Android Open Source Project
|
||||
|
||||
LOCAL_SRC_FILES += qemu_tracing/qemu_tracing.c
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// This is the pathname to the sysfs file that enables and disables
|
||||
// tracing on the qemu emulator.
|
||||
#define SYS_QEMU_TRACE_STATE "/sys/qemu_trace/state"
|
||||
|
||||
|
||||
// This is the pathname to the sysfs file that adds new (address, symbol)
|
||||
// pairs to the trace.
|
||||
#define SYS_QEMU_TRACE_SYMBOL "/sys/qemu_trace/symbol"
|
||||
|
||||
// The maximum length of a symbol name
|
||||
#define MAX_SYMBOL_NAME_LENGTH (4 * 1024)
|
||||
|
||||
// Allow space in the buffer for the address plus whitespace.
|
||||
#define MAX_BUF_SIZE (MAX_SYMBOL_NAME_LENGTH + 20)
|
||||
|
||||
// return 0 on success, or an error if the qemu driver cannot be opened
|
||||
int qemu_start_tracing()
|
||||
{
|
||||
int fd = open(SYS_QEMU_TRACE_STATE, O_WRONLY);
|
||||
if (fd < 0)
|
||||
return fd;
|
||||
write(fd, "1\n", 2);
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int qemu_stop_tracing()
|
||||
{
|
||||
int fd = open(SYS_QEMU_TRACE_STATE, O_WRONLY);
|
||||
if (fd < 0)
|
||||
return fd;
|
||||
write(fd, "0\n", 2);
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int qemu_add_mapping(unsigned int addr, const char *name)
|
||||
{
|
||||
char buf[MAX_BUF_SIZE];
|
||||
|
||||
if (strlen(name) > MAX_SYMBOL_NAME_LENGTH)
|
||||
return EINVAL;
|
||||
int fd = open(SYS_QEMU_TRACE_SYMBOL, O_WRONLY);
|
||||
if (fd < 0)
|
||||
return fd;
|
||||
sprintf(buf, "%x %s\n", addr, name);
|
||||
write(fd, buf, strlen(buf));
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int qemu_remove_mapping(unsigned int addr)
|
||||
{
|
||||
char buf[MAX_BUF_SIZE];
|
||||
|
||||
int fd = open(SYS_QEMU_TRACE_SYMBOL, O_WRONLY);
|
||||
if (fd < 0)
|
||||
return fd;
|
||||
sprintf(buf, "%x\n", addr);
|
||||
write(fd, buf, strlen(buf));
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
Loading…
Reference in a new issue