mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2025-07-05 20:41:34 -07:00
SWIG: test callback & output grabbing
This commit is contained in:
parent
c7e5648164
commit
39710c5fdf
26 changed files with 8332 additions and 6950 deletions
|
@ -289,11 +289,11 @@ endif
|
|||
# SWIG #
|
||||
########
|
||||
|
||||
ifneq ("$(wildcard src/pm3_luawrap.c)","")
|
||||
ifneq ("$(wildcard src/pm3_luawrap.cpp)","")
|
||||
SWIG_LUA_FOUND = 1
|
||||
endif
|
||||
ifeq ($(PYTHON_FOUND),1)
|
||||
ifneq ("$(wildcard src/pm3_pywrap.c)","")
|
||||
ifneq ("$(wildcard src/pm3_pywrap.cpp)","")
|
||||
SWIG_PYTHON_FOUND = 1
|
||||
endif
|
||||
endif
|
||||
|
@ -305,6 +305,10 @@ PM3CFLAGS = $(CFLAGS)
|
|||
PM3CFLAGS += -I./src -I./include -I../include -I../common -I../common_fpga $(INCLUDES)
|
||||
# WIP Testing
|
||||
#PM3CFLAGS += -std=c11 -pedantic
|
||||
CXXFLAGS ?= -Wall -Werror -O3
|
||||
PM3CXXFLAGS = $(CXXFLAGS)
|
||||
PM3CXXFLAGS += -I../include -I./include -I../common $(INCLUDES)
|
||||
|
||||
|
||||
PREFIX ?= /usr/local
|
||||
ifneq (,$(findstring MINGW,$(platform)))
|
||||
|
@ -330,15 +334,13 @@ endif
|
|||
|
||||
ifeq ($(SWIG_LUA_FOUND),1)
|
||||
PM3CFLAGS += -DHAVE_LUA_SWIG
|
||||
PM3CXXFLAGS += -DHAVE_LUA_SWIG
|
||||
endif
|
||||
ifeq ($(SWIG_PYTHON_FOUND),1)
|
||||
PM3CFLAGS += -DHAVE_PYTHON_SWIG
|
||||
PM3CXXFLAGS += -DHAVE_PYTHON_SWIG
|
||||
endif
|
||||
|
||||
CXXFLAGS ?= -Wall -Werror -O3
|
||||
PM3CXXFLAGS = $(CXXFLAGS)
|
||||
PM3CXXFLAGS += -I../include -I./include
|
||||
|
||||
ifeq ($(QT_FOUND),1)
|
||||
PM3CFLAGS += -DHAVE_GUI
|
||||
PM3CXXFLAGS += -DQT_NO_DEBUG
|
||||
|
@ -568,7 +570,6 @@ SRCS = aiddesfire.c \
|
|||
mifare/mifaredefault.c \
|
||||
mifare/mifarehost.c \
|
||||
mifare/ndef.c \
|
||||
pm3.c \
|
||||
pm3_binlib.c \
|
||||
pm3_bitlib.c \
|
||||
preferences.c \
|
||||
|
@ -605,10 +606,10 @@ SRCS += bucketsort.c \
|
|||
|
||||
SWIGSRCS =
|
||||
ifeq ($(SWIG_LUA_FOUND),1)
|
||||
SWIGSRCS += pm3_luawrap.c
|
||||
SWIGSRCS += pm3_luawrap.cpp
|
||||
endif
|
||||
ifeq ($(SWIG_PYTHON_FOUND),1)
|
||||
SWIGSRCS += pm3_pywrap.c
|
||||
SWIGSRCS += pm3_pywrap.cpp
|
||||
endif
|
||||
|
||||
# gui
|
||||
|
@ -617,6 +618,7 @@ ifeq ($(QT_FOUND),1)
|
|||
else
|
||||
CXXSRCS = guidummy.cpp
|
||||
endif
|
||||
CXXSRCS += pm3.cpp
|
||||
|
||||
# OS X
|
||||
ifeq ($(platform),Darwin)
|
||||
|
@ -624,7 +626,7 @@ ifeq ($(platform),Darwin)
|
|||
endif
|
||||
|
||||
OBJS = $(SRCS:%.c=$(OBJDIR)/%.o)
|
||||
OBJS += $(SWIGSRCS:%.c=$(OBJDIR)/%.o)
|
||||
OBJS += $(SWIGSRCS:%.cpp=$(OBJDIR)/%.o)
|
||||
OBJS += $(CXXSRCS:%.cpp=$(OBJDIR)/%.o)
|
||||
OBJS += $(OBJCSRCS:%.m=$(OBJDIR)/%.o)
|
||||
|
||||
|
@ -756,13 +758,13 @@ endif
|
|||
# SWIG #
|
||||
########
|
||||
|
||||
src/pm3_luawrap.c: pm3.i
|
||||
src/pm3_luawrap.cpp: pm3.i
|
||||
$(info [=] GEN $@)
|
||||
$(Q)$(SWIG) -lua -o $@ $<
|
||||
$(Q)$(SWIG) -c++ -lua -o $@ $<
|
||||
|
||||
src/pm3_pywrap.c: pm3.i
|
||||
src/pm3_pywrap.cpp: pm3.i
|
||||
$(info [=] GEN $@)
|
||||
$(Q)$(SWIG) -python -o $@ $<
|
||||
$(Q)$(SWIG) -c++ -python -o $@ $<
|
||||
|
||||
########
|
||||
# misc #
|
||||
|
@ -779,11 +781,12 @@ src/version.c: default_version.c
|
|||
print-%: ; @echo $* = $($*)
|
||||
|
||||
# SWIG files emit a number of warnings, we've to ignore them
|
||||
%wrap.o: %wrap.c
|
||||
$(OBJDIR)/%wrap.o : %wrap.c $(OBJDIR)/%.d
|
||||
$(info [-] CC $<)
|
||||
%wrap.o: %wrap.cpp
|
||||
$(OBJDIR)/%wrap.o : %wrap.cpp $(OBJDIR)/%.d
|
||||
$(info [-] CXX $<)
|
||||
$(Q)$(MKDIR) $(dir $@)
|
||||
$(Q)$(CC) $(DEPFLAGS) $(PM3CFLAGS) -Wno-missing-prototypes -Wno-missing-declarations -Wno-missing-field-initializers -c -o $@ $<
|
||||
# $(Q)$(CXX) $(DEPFLAGS) $(PM3CXXFLAGS) -Wno-missing-prototypes -Wno-missing-declarations -Wno-missing-field-initializers -c -o $@ $<
|
||||
$(Q)$(CXX) $(DEPFLAGS) $(PM3CXXFLAGS) -c -o $@ $<
|
||||
$(Q)$(POSTCOMPILE)
|
||||
|
||||
%.o: %.c
|
||||
|
@ -808,7 +811,7 @@ $(OBJDIR)/%.o : %.m $(OBJDIR)/%.d
|
|||
$(Q)$(POSTCOMPILE)
|
||||
|
||||
DEPENDENCY_FILES = $(patsubst %.c, $(OBJDIR)/%.d, $(SRCS)) \
|
||||
$(patsubst %wrap.c, $(OBJDIR)/%.d, $(SWIGSRCS)) \
|
||||
$(patsubst %wrap.cpp, $(OBJDIR)/%.d, $(SWIGSRCS)) \
|
||||
$(patsubst %.cpp, $(OBJDIR)/%.d, $(CXXSRCS)) \
|
||||
$(patsubst %.m, $(OBJDIR)/%.d, $(OBJCSRCS))
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd ..
|
||||
make src/pm3_luawrap.c
|
||||
make src/pm3_pywrap.c
|
||||
make src/pm3_luawrap.cpp
|
||||
make src/pm3_pywrap.cpp
|
||||
|
|
5
client/experimental_client_with_swig/02b_run_test_py_cb.sh
Executable file
5
client/experimental_client_with_swig/02b_run_test_py_cb.sh
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
../../pm3 -c "script run testembedded_cb.py"
|
||||
# cf https://github.com/swig/swig/tree/master/Examples/python/callback
|
||||
# cf https://rawgit.com/swig/swig/master/Doc/Manual/SWIGPlus.html#SWIGPlus_target_language_callbacks
|
2
client/experimental_client_with_swig/example/README.md
Normal file
2
client/experimental_client_with_swig/example/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
http://www.swig.org/Doc4.0/SWIG.html#SWIG_nn30
|
||||
http://www.swig.org/Doc4.0/SWIGPlus.html#SWIGPlus_target_language_callbacks
|
4
client/experimental_client_with_swig/example/example.cxx
Normal file
4
client/experimental_client_with_swig/example/example.cxx
Normal file
|
@ -0,0 +1,4 @@
|
|||
/* File : example.cxx */
|
||||
|
||||
#include "example.h"
|
||||
|
22
client/experimental_client_with_swig/example/example.h
Normal file
22
client/experimental_client_with_swig/example/example.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
/* File : example.h */
|
||||
|
||||
#include <iostream>
|
||||
|
||||
class Callback {
|
||||
public:
|
||||
virtual ~Callback() { std::cout << "Callback::~Callback()" << std:: endl; }
|
||||
virtual void run() { std::cout << "Callback::run()" << std::endl; }
|
||||
};
|
||||
|
||||
|
||||
class Caller {
|
||||
private:
|
||||
Callback *_callback;
|
||||
public:
|
||||
Caller(): _callback(0) {}
|
||||
~Caller() { delCallback(); }
|
||||
void delCallback() { delete _callback; _callback = 0; }
|
||||
void setCallback(Callback *cb) { delCallback(); _callback = cb; }
|
||||
void call() { if (_callback) _callback->run(); }
|
||||
};
|
||||
|
11
client/experimental_client_with_swig/example/example.i
Normal file
11
client/experimental_client_with_swig/example/example.i
Normal file
|
@ -0,0 +1,11 @@
|
|||
/* File : example.i */
|
||||
%module(directors="1") example
|
||||
%{
|
||||
#include "example.h"
|
||||
%}
|
||||
|
||||
/* turn on director wrapping Callback */
|
||||
%feature("director") Callback;
|
||||
|
||||
%include "example.h"
|
||||
|
57
client/experimental_client_with_swig/example/runme.py
Normal file
57
client/experimental_client_with_swig/example/runme.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
# file: runme.py
|
||||
|
||||
# This file illustrates the cross language polymorphism using directors.
|
||||
|
||||
import example
|
||||
|
||||
|
||||
class PyCallback(example.Callback):
|
||||
|
||||
def __init__(self):
|
||||
example.Callback.__init__(self)
|
||||
|
||||
def run(self):
|
||||
print("PyCallback.run()")
|
||||
|
||||
# Create an Caller instance
|
||||
|
||||
caller = example.Caller()
|
||||
|
||||
# Add a simple C++ callback (caller owns the callback, so
|
||||
# we disown it first by clearing the .thisown flag).
|
||||
|
||||
print("Adding and calling a normal C++ callback")
|
||||
print("----------------------------------------")
|
||||
|
||||
callback = example.Callback()
|
||||
callback.thisown = 0
|
||||
caller.setCallback(callback)
|
||||
caller.call()
|
||||
caller.delCallback()
|
||||
|
||||
print("")
|
||||
print("Adding and calling a Python callback")
|
||||
print("------------------------------------")
|
||||
|
||||
# Add a Python callback (caller owns the callback, so we
|
||||
# disown it first by calling __disown__).
|
||||
|
||||
caller.setCallback(PyCallback().__disown__())
|
||||
caller.call()
|
||||
caller.delCallback()
|
||||
|
||||
print("")
|
||||
print("Adding and calling another Python callback")
|
||||
print("------------------------------------------")
|
||||
|
||||
# Let's do the same but use the weak reference this time.
|
||||
|
||||
callback = PyCallback().__disown__()
|
||||
caller.setCallback(callback)
|
||||
caller.call()
|
||||
caller.delCallback()
|
||||
|
||||
# All done.
|
||||
|
||||
print("")
|
||||
print("python exit")
|
21
client/experimental_client_with_swig/testembedded_cb.py
Executable file
21
client/experimental_client_with_swig/testembedded_cb.py
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import pm3
|
||||
p=pm3.pm3()
|
||||
|
||||
# PyConsoleHandler class is defined and derived from C++ class ConsoleHandler
|
||||
class PyConsoleHandler(pm3.ConsoleHandler):
|
||||
def __init__(self):
|
||||
pm3.ConsoleHandler.__init__(self)
|
||||
def handle_output(self, c):
|
||||
print("PY>>", c, end='')
|
||||
# don't let original print routine pursuing:
|
||||
return 0
|
||||
|
||||
#p.console("hw status")
|
||||
|
||||
handler = PyConsoleHandler()
|
||||
result = p.console_async_wrapper("hw status", handler)
|
||||
print(result)
|
||||
|
||||
print("Device:", p.name)
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd ..
|
||||
make src/pm3_luawrap.c
|
||||
make src/pm3_pywrap.c
|
||||
make src/pm3_luawrap.cpp
|
||||
make src/pm3_pywrap.cpp
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
#ifndef LIBPM3_H
|
||||
#define LIBPM3_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct pm3_device pm3;
|
||||
|
||||
pm3 *pm3_open(char *port);
|
||||
|
||||
// not catching output
|
||||
int pm3_console(pm3 *dev, char *cmd);
|
||||
// catching output as it comes
|
||||
int pm3_console_async(pm3 *dev, char *cmd, int (*callback)(char* s));
|
||||
// catching output at the end
|
||||
//int pm3_console_sync(pm3 *dev, char *cmd, char* outbuf, int outbufsize);
|
||||
|
||||
const char *pm3_name_get(pm3 *dev);
|
||||
void pm3_close(pm3 *dev);
|
||||
pm3 *pm3_get_current_dev(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif // LIBPM3_H
|
||||
|
|
8
client/include/pm3.hpp
Normal file
8
client/include/pm3.hpp
Normal file
|
@ -0,0 +1,8 @@
|
|||
#ifndef LIBPM3PP_H
|
||||
#define LIBPM3PP_H
|
||||
|
||||
#include "pm3.h"
|
||||
#include "pm3_helper.hpp"
|
||||
int pm3_console_async_wrapper(pm3 *dev, char *cmd, ConsoleHandler *console_handler);
|
||||
|
||||
#endif // LIBPM3PP_H
|
10
client/include/pm3_helper.hpp
Normal file
10
client/include/pm3_helper.hpp
Normal file
|
@ -0,0 +1,10 @@
|
|||
#ifndef LIBPM3HELPERPP_H
|
||||
#define LIBPM3HELPERPP_H
|
||||
|
||||
class ConsoleHandler {
|
||||
public:
|
||||
virtual int handle_output(char *string) = 0;
|
||||
virtual ~ConsoleHandler() {}
|
||||
};
|
||||
|
||||
#endif // LIBPM3HELPERPP_H
|
|
@ -322,6 +322,13 @@ int CommandReceived(char *Cmd) {
|
|||
return CmdsParse(CommandTable, Cmd);
|
||||
}
|
||||
|
||||
int CommandReceivedCB(char *Cmd, print_cb_t callback) {
|
||||
g_printCallback = callback;
|
||||
int res = CmdsParse(CommandTable, Cmd);
|
||||
g_printCallback = NULL;
|
||||
return res;
|
||||
}
|
||||
|
||||
command_t *getTopLevelCommandTable(void) {
|
||||
return CommandTable;
|
||||
}
|
||||
|
|
|
@ -13,9 +13,18 @@
|
|||
|
||||
#include "common.h"
|
||||
#include "cmdparser.h" // command_t
|
||||
#include "util.h" // print_cb_t
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int CommandReceived(char *Cmd);
|
||||
int CmdRem(const char *Cmd);
|
||||
command_t *getTopLevelCommandTable(void);
|
||||
int CommandReceivedCB(char *Cmd, print_cb_t callback);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// User API
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include "pm3.h"
|
||||
#include "pm3.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
|
@ -46,6 +46,23 @@ int pm3_console(pm3_device *dev, char *Cmd) {
|
|||
return CommandReceived(Cmd);
|
||||
}
|
||||
|
||||
static ConsoleHandler *console_handler_ptr = NULL;
|
||||
static int console_handler_helper(char *string) {
|
||||
return console_handler_ptr->handle_output(string);
|
||||
}
|
||||
int pm3_console_async_wrapper(pm3_device *dev, char *Cmd, ConsoleHandler *console_handler) {
|
||||
console_handler_ptr = console_handler;
|
||||
int result = pm3_console_async(dev, Cmd, &console_handler_helper);
|
||||
console_handler = NULL; // ?? console_handler_ptr = NULL cf http://www.swig.org/Doc4.0/SWIGPlus.html#SWIGPlus_target_language_callbacks
|
||||
return result;
|
||||
}
|
||||
|
||||
int pm3_console_async(pm3_device *dev, char *Cmd, int (*callback)(char*)) {
|
||||
// For now, there is no real device context:
|
||||
(void) dev;
|
||||
return CommandReceivedCB(Cmd, callback);
|
||||
}
|
||||
|
||||
const char *pm3_name_get(pm3_device *dev) {
|
||||
return dev->conn->serial_port_name;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
%module pm3
|
||||
%module(directors="1") pm3
|
||||
%{
|
||||
/* Include the header in the wrapper code */
|
||||
#include "pm3.h"
|
||||
#include "pm3.hpp"
|
||||
#include "comms.h"
|
||||
%}
|
||||
|
||||
|
@ -31,9 +31,15 @@ typedef struct {
|
|||
}
|
||||
}
|
||||
int console(char *cmd);
|
||||
//int console_async(char *cmd, int (*callback)(char*));
|
||||
int console_async_wrapper(char *cmd, ConsoleHandler *handler);
|
||||
char const * const name;
|
||||
}
|
||||
} pm3;
|
||||
//%nodefaultctor device;
|
||||
//%nodefaultdtor device;
|
||||
/* Parse the header file to generate wrappers */
|
||||
|
||||
%feature("director") ConsoleHandler;
|
||||
|
||||
%include "../include/pm3_helper.hpp"
|
||||
|
|
|
@ -61,6 +61,8 @@ class _SwigNonDynamicMeta(type):
|
|||
__setattr__ = _swig_setattr_nondynamic_class_variable(type.__setattr__)
|
||||
|
||||
|
||||
import weakref
|
||||
|
||||
class pm3(object):
|
||||
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
|
||||
__repr__ = _swig_repr
|
||||
|
@ -71,10 +73,35 @@ class pm3(object):
|
|||
|
||||
def console(self, cmd):
|
||||
return _pm3.pm3_console(self, cmd)
|
||||
|
||||
def console_async_wrapper(self, cmd, handler):
|
||||
return _pm3.pm3_console_async_wrapper(self, cmd, handler)
|
||||
name = property(_pm3.pm3_name_get)
|
||||
|
||||
# Register pm3 in _pm3:
|
||||
_pm3.pm3_swigregister(pm3)
|
||||
|
||||
class ConsoleHandler(object):
|
||||
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
|
||||
__repr__ = _swig_repr
|
||||
|
||||
def handle_output(self, string):
|
||||
return _pm3.ConsoleHandler_handle_output(self, string)
|
||||
__swig_destroy__ = _pm3.delete_ConsoleHandler
|
||||
|
||||
def __init__(self):
|
||||
if self.__class__ == ConsoleHandler:
|
||||
_self = None
|
||||
else:
|
||||
_self = self
|
||||
_pm3.ConsoleHandler_swiginit(self, _pm3.new_ConsoleHandler(_self, ))
|
||||
def __disown__(self):
|
||||
self.this.disown()
|
||||
_pm3.disown_ConsoleHandler(self)
|
||||
return weakref.proxy(self)
|
||||
|
||||
# Register ConsoleHandler in _pm3:
|
||||
_pm3.ConsoleHandler_swigregister(ConsoleHandler)
|
||||
|
||||
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
3396
client/src/pm3_luawrap.cpp
Normal file
3396
client/src/pm3_luawrap.cpp
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
4666
client/src/pm3_pywrap.cpp
Normal file
4666
client/src/pm3_pywrap.cpp
Normal file
File diff suppressed because it is too large
Load diff
|
@ -308,6 +308,7 @@ static void fPrintAndLog(FILE *stream, const char *fmt, ...) {
|
|||
if (logging && session.incognito) {
|
||||
logging = 0;
|
||||
}
|
||||
// TODO log if callback ?
|
||||
if ((g_printAndLog & PRINTANDLOG_LOG) && logging && !logfile) {
|
||||
char *my_logfile_path = NULL;
|
||||
char filename[40];
|
||||
|
@ -367,9 +368,14 @@ static void fPrintAndLog(FILE *stream, const char *fmt, ...) {
|
|||
memcpy_filter_ansi(buffer2, buffer, sizeof(buffer), filter_ansi);
|
||||
if (g_printAndLog & PRINTANDLOG_PRINT) {
|
||||
memcpy_filter_emoji(buffer3, buffer2, sizeof(buffer2), session.emoji_mode);
|
||||
fprintf(stream, "%s", buffer3);
|
||||
if (linefeed)
|
||||
fprintf(stream, "\n");
|
||||
if (linefeed && (strlen(buffer3) + 1 < sizeof(buffer3)))
|
||||
buffer3[strlen(buffer3)]='\n';
|
||||
bool doprint = true;
|
||||
if (g_printCallback != NULL) {
|
||||
doprint = g_printCallback(buffer3) != 0;
|
||||
}
|
||||
if (doprint)
|
||||
fprintf(stream, "%s", buffer3);
|
||||
}
|
||||
|
||||
#ifdef RL_STATE_READCMD
|
||||
|
|
|
@ -30,6 +30,8 @@
|
|||
uint8_t g_debugMode = 0;
|
||||
// global client disable logging variable
|
||||
uint8_t g_printAndLog = PRINTANDLOG_PRINT | PRINTANDLOG_LOG;
|
||||
// global client print callback
|
||||
print_cb_t g_printCallback;
|
||||
// global client tell if a pending prompt is present
|
||||
bool g_pendingPrompt = false;
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
|
||||
extern uint8_t g_debugMode;
|
||||
extern uint8_t g_printAndLog;
|
||||
typedef int (*print_cb_t)(char *s);
|
||||
extern print_cb_t g_printCallback;
|
||||
extern bool g_pendingPrompt;
|
||||
|
||||
#define PRINTANDLOG_PRINT 1
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
|
||||
#include "common.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
# include <windows.h>
|
||||
# define sleep(n) Sleep(1000 *(n))
|
||||
|
@ -23,4 +27,7 @@ void msleep(uint32_t n); // sleep n milliseconds
|
|||
|
||||
uint64_t msclock(void); // a milliseconds clock
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue