mirror of
https://github.com/postgres/postgres.git
synced 2025-07-28 23:42:10 +03:00
What I've done:
1. Rewritten libpq to allow asynchronous clients. 2. Implemented client side of cancel protocol in library, and patched psql.c to send a cancel request upon SIGINT. The backend doesn't notice it yet :-( 3. Implemented 'Z' protocol message addition and renaming of copy in/out start messages. These are implemented conditionally, ie, the client protocol version is checked; so the code should still work with 1.0 clients. 4. Revised protocol and libpq sgml documents (don't have an SGML compiler, though, so there may be some markup glitches here). What remains to be done: 1. Implement addition of atttypmod field to RowDescriptor messages. The client-side code is there but ifdef'd out. I have no idea what to change on the backend side. The field should be sent only if protocol >= 2.0, of course. 2. Implement backend response to cancel requests received as OOB messages. (This prolly need not be conditional on protocol version; just do it if you get SIGURG.) 3. Update libpq.3. (I'm hoping this can be generated mechanically from libpq.sgml... if not, will do it by hand.) Is there any other doco to fix? 4. Update non-libpq interfaces as necessary. I patched libpgtcl so that it would compile, but haven't tested it. Dunno what needs to be done with the other interfaces. Have at it! Tom Lane
This commit is contained in:
@ -5,177 +5,496 @@
|
||||
*
|
||||
* DESCRIPTION
|
||||
* miscellaneous useful functions
|
||||
* these routines are analogous to the ones in libpq/pqcomm.c
|
||||
*
|
||||
* The communication routines here are analogous to the ones in
|
||||
* backend/libpq/pqcomm.c and backend/libpq/pqcomprim.c, but operate
|
||||
* in the considerably different environment of the frontend libpq.
|
||||
* In particular, we work with a bare nonblock-mode socket, rather than
|
||||
* a stdio stream, so that we can avoid unwanted blocking of the application.
|
||||
*
|
||||
* XXX: MOVE DEBUG PRINTOUT TO HIGHER LEVEL. As is, block and restart
|
||||
* will cause repeat printouts.
|
||||
*
|
||||
* We must speak the same transmitted data representations as the backend
|
||||
* routines. Note that this module supports *only* network byte order
|
||||
* for transmitted ints, whereas the backend modules (as of this writing)
|
||||
* still handle either network or little-endian byte order.
|
||||
*
|
||||
* Copyright (c) 1994, Regents of the University of California
|
||||
*
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* $Header: /cvsroot/pgsql/src/interfaces/libpq/fe-misc.c,v 1.10 1998/02/26 04:45:09 momjian Exp $
|
||||
* $Header: /cvsroot/pgsql/src/interfaces/libpq/fe-misc.c,v 1.11 1998/05/06 23:51:14 momjian Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#if !defined(NO_UNISTD_H)
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <sys/types.h> /* for fd_set stuff */
|
||||
#ifdef HAVE_SYS_SELECT_H
|
||||
#include <sys/select.h>
|
||||
#endif
|
||||
|
||||
#include "postgres.h"
|
||||
|
||||
#include "libpq-fe.h"
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pqGetc:
|
||||
get a character from stream f
|
||||
get a character from the connection
|
||||
|
||||
if debug is set, also echo the character fetched
|
||||
All these routines return 0 on success, EOF on error.
|
||||
Note that for the Get routines, EOF only means there is not enough
|
||||
data in the buffer, not that there is necessarily a hard error.
|
||||
*/
|
||||
int
|
||||
pqGetc(FILE *fin, FILE *debug)
|
||||
pqGetc(char *result, PGconn *conn)
|
||||
{
|
||||
int c;
|
||||
if (conn->inCursor >= conn->inEnd)
|
||||
return EOF;
|
||||
|
||||
c = getc(fin);
|
||||
*result = conn->inBuffer[conn->inCursor++];
|
||||
|
||||
if (debug && c != EOF)
|
||||
fprintf(debug, "From backend> %c\n", c);
|
||||
if (conn->Pfdebug)
|
||||
fprintf(conn->Pfdebug, "From backend> %c\n", *result);
|
||||
|
||||
return c;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pqPutnchar:
|
||||
send a string of exactly len length into stream f
|
||||
|
||||
returns 1 if there was an error, 0 otherwise.
|
||||
*/
|
||||
int
|
||||
pqPutnchar(const char *s, int len, FILE *f, FILE *debug)
|
||||
{
|
||||
if (debug)
|
||||
fprintf(debug, "To backend> %s\n", s);
|
||||
|
||||
return (pqPutNBytes(s, len, f) == EOF ? 1 : 0);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pqGetnchar:
|
||||
get a string of exactly len bytes in buffer s (which must be 1 byte
|
||||
longer) from stream f and terminate it with a '\0'.
|
||||
*/
|
||||
int
|
||||
pqGetnchar(char *s, int len, FILE *f, FILE *debug)
|
||||
/* pqPutBytes: local routine to write N bytes to the connection,
|
||||
with buffering
|
||||
*/
|
||||
static int
|
||||
pqPutBytes(const char *s, int nbytes, PGconn *conn)
|
||||
{
|
||||
int status;
|
||||
int avail = conn->outBufSize - conn->outCount;
|
||||
|
||||
status = pqGetNBytes(s, len, f);
|
||||
|
||||
if (debug)
|
||||
fprintf(debug, "From backend (%d)> %s\n", len, s);
|
||||
|
||||
return (status == EOF ? 1 : 0);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pqGets:
|
||||
get a string of up to length len from stream f
|
||||
*/
|
||||
int
|
||||
pqGets(char *s, int len, FILE *f, FILE *debug)
|
||||
{
|
||||
int status;
|
||||
|
||||
status = pqGetString(s, len, f);
|
||||
|
||||
if (debug)
|
||||
fprintf(debug, "From backend> \"%s\"\n", s);
|
||||
|
||||
return (status == EOF ? 1 : 0);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pgPutInt
|
||||
send an integer of 2 or 4 bytes to the file stream, compensate
|
||||
for host endianness.
|
||||
returns 0 if successful, 1 otherwise
|
||||
*/
|
||||
int
|
||||
pqPutInt(const int integer, int bytes, FILE *f, FILE *debug)
|
||||
{
|
||||
int retval = 0;
|
||||
|
||||
switch (bytes)
|
||||
while (nbytes > avail)
|
||||
{
|
||||
case 2:
|
||||
retval = pqPutShort(integer, f);
|
||||
break;
|
||||
case 4:
|
||||
retval = pqPutLong(integer, f);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "** int size %d not supported\n", bytes);
|
||||
retval = 1;
|
||||
memcpy(conn->outBuffer + conn->outCount, s, avail);
|
||||
conn->outCount += avail;
|
||||
s += avail;
|
||||
nbytes -= avail;
|
||||
if (pqFlush(conn))
|
||||
return EOF;
|
||||
avail = conn->outBufSize;
|
||||
}
|
||||
|
||||
if (debug)
|
||||
fprintf(debug, "To backend (%d#)> %d\n", bytes, integer);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pgGetInt
|
||||
read a 2 or 4 byte integer from the stream and swab it around
|
||||
to compensate for different endianness
|
||||
returns 0 if successful
|
||||
*/
|
||||
int
|
||||
pqGetInt(int *result, int bytes, FILE *f, FILE *debug)
|
||||
{
|
||||
int retval = 0;
|
||||
|
||||
switch (bytes)
|
||||
{
|
||||
case 2:
|
||||
retval = pqGetShort(result, f);
|
||||
break;
|
||||
case 4:
|
||||
retval = pqGetLong(result, f);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "** int size %d not supported\n", bytes);
|
||||
retval = 1;
|
||||
}
|
||||
|
||||
if (debug)
|
||||
fprintf(debug, "From backend (#%d)> %d\n", bytes, *result);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
int
|
||||
pqPuts(const char *s, FILE *f, FILE *debug)
|
||||
{
|
||||
if (pqPutString(s, f) == EOF)
|
||||
return 1;
|
||||
|
||||
fflush(f);
|
||||
|
||||
if (debug)
|
||||
fprintf(debug, "To backend> %s\n", s);
|
||||
memcpy(conn->outBuffer + conn->outCount, s, nbytes);
|
||||
conn->outCount += nbytes;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
void
|
||||
pqFlush(FILE *f, FILE *debug)
|
||||
/* pqGets:
|
||||
get a null-terminated string from the connection,
|
||||
and store it in a buffer of size maxlen bytes.
|
||||
If the incoming string is >= maxlen bytes, all of it is read,
|
||||
but the excess characters are silently discarded.
|
||||
*/
|
||||
int
|
||||
pqGets(char *s, int maxlen, PGconn *conn)
|
||||
{
|
||||
if (f)
|
||||
fflush(f);
|
||||
/* Copy conn data to locals for faster search loop */
|
||||
char *inBuffer = conn->inBuffer;
|
||||
int inCursor = conn->inCursor;
|
||||
int inEnd = conn->inEnd;
|
||||
int slen;
|
||||
|
||||
if (debug)
|
||||
fflush(debug);
|
||||
while (inCursor < inEnd && inBuffer[inCursor])
|
||||
inCursor++;
|
||||
|
||||
if (inCursor >= inEnd)
|
||||
return EOF;
|
||||
|
||||
slen = inCursor - conn->inCursor;
|
||||
if (slen < maxlen)
|
||||
strcpy(s, inBuffer + conn->inCursor);
|
||||
else
|
||||
{
|
||||
strncpy(s, inBuffer + conn->inCursor, maxlen-1);
|
||||
s[maxlen-1] = '\0';
|
||||
}
|
||||
|
||||
conn->inCursor = ++inCursor;
|
||||
|
||||
if (conn->Pfdebug)
|
||||
fprintf(conn->Pfdebug, "From backend> \"%s\"\n", s);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
int
|
||||
pqPuts(const char *s, PGconn *conn)
|
||||
{
|
||||
if (pqPutBytes(s, strlen(s)+1, conn))
|
||||
return EOF;
|
||||
|
||||
if (conn->Pfdebug)
|
||||
fprintf(conn->Pfdebug, "To backend> %s\n", s);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pqGetnchar:
|
||||
get a string of exactly len bytes in buffer s (which must be 1 byte
|
||||
longer) and terminate it with a '\0'.
|
||||
*/
|
||||
int
|
||||
pqGetnchar(char *s, int len, PGconn *conn)
|
||||
{
|
||||
if (len < 0 || len > conn->inEnd - conn->inCursor)
|
||||
return EOF;
|
||||
|
||||
memcpy(s, conn->inBuffer + conn->inCursor, len);
|
||||
s[len] = '\0';
|
||||
|
||||
conn->inCursor += len;
|
||||
|
||||
if (conn->Pfdebug)
|
||||
fprintf(conn->Pfdebug, "From backend (%d)> %s\n", len, s);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pqPutnchar:
|
||||
send a string of exactly len bytes
|
||||
The buffer should have a terminating null, but it's not sent.
|
||||
*/
|
||||
int
|
||||
pqPutnchar(const char *s, int len, PGconn *conn)
|
||||
{
|
||||
if (pqPutBytes(s, len, conn))
|
||||
return EOF;
|
||||
|
||||
if (conn->Pfdebug)
|
||||
fprintf(conn->Pfdebug, "To backend> %s\n", s);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pgGetInt
|
||||
read a 2 or 4 byte integer and convert from network byte order
|
||||
to local byte order
|
||||
*/
|
||||
int
|
||||
pqGetInt(int *result, int bytes, PGconn *conn)
|
||||
{
|
||||
uint16 tmp2;
|
||||
uint32 tmp4;
|
||||
|
||||
switch (bytes)
|
||||
{
|
||||
case 2:
|
||||
if (conn->inCursor + 2 > conn->inEnd)
|
||||
return EOF;
|
||||
memcpy(&tmp2, conn->inBuffer + conn->inCursor, 2);
|
||||
conn->inCursor += 2;
|
||||
*result = (int) ntohs(tmp2);
|
||||
break;
|
||||
case 4:
|
||||
if (conn->inCursor + 4 > conn->inEnd)
|
||||
return EOF;
|
||||
memcpy(&tmp4, conn->inBuffer + conn->inCursor, 4);
|
||||
conn->inCursor += 4;
|
||||
*result = (int) ntohl(tmp4);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "** int size %d not supported\n", bytes);
|
||||
return EOF;
|
||||
}
|
||||
|
||||
if (conn->Pfdebug)
|
||||
fprintf(conn->Pfdebug, "From backend (#%d)> %d\n", bytes, *result);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pgPutInt
|
||||
send an integer of 2 or 4 bytes, converting from host byte order
|
||||
to network byte order.
|
||||
*/
|
||||
int
|
||||
pqPutInt(int value, int bytes, PGconn *conn)
|
||||
{
|
||||
uint16 tmp2;
|
||||
uint32 tmp4;
|
||||
|
||||
switch (bytes)
|
||||
{
|
||||
case 2:
|
||||
tmp2 = htons((uint16) value);
|
||||
if (pqPutBytes((const char*) &tmp2, 2, conn))
|
||||
return EOF;
|
||||
break;
|
||||
case 4:
|
||||
tmp4 = htonl((uint32) value);
|
||||
if (pqPutBytes((const char*) &tmp4, 4, conn))
|
||||
return EOF;
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "** int size %d not supported\n", bytes);
|
||||
return EOF;
|
||||
}
|
||||
|
||||
if (conn->Pfdebug)
|
||||
fprintf(conn->Pfdebug, "To backend (%d#)> %d\n", bytes, value);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pqReadReady: is select() saying the file is ready to read?
|
||||
*/
|
||||
static int
|
||||
pqReadReady(PGconn *conn)
|
||||
{
|
||||
fd_set input_mask;
|
||||
struct timeval timeout;
|
||||
|
||||
if (conn->sock < 0)
|
||||
return 0;
|
||||
|
||||
FD_ZERO(&input_mask);
|
||||
FD_SET(conn->sock, &input_mask);
|
||||
timeout.tv_sec = 0;
|
||||
timeout.tv_usec = 0;
|
||||
if (select(conn->sock+1, &input_mask, (fd_set *) NULL, (fd_set *) NULL,
|
||||
&timeout) < 0)
|
||||
{
|
||||
sprintf(conn->errorMessage,
|
||||
"pqReadReady() -- select() failed: errno=%d\n%s\n",
|
||||
errno, strerror(errno));
|
||||
return 0;
|
||||
}
|
||||
return FD_ISSET(conn->sock, &input_mask);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pqReadData: read more data, if any is available
|
||||
* Possible return values:
|
||||
* 1: successfully loaded at least one more byte
|
||||
* 0: no data is presently available, but no error detected
|
||||
* -1: error detected (including EOF = connection closure);
|
||||
* conn->errorMessage set
|
||||
* NOTE: callers must not assume that pointers or indexes into conn->inBuffer
|
||||
* remain valid across this call!
|
||||
*/
|
||||
int
|
||||
pqReadData(PGconn *conn)
|
||||
{
|
||||
int nread;
|
||||
|
||||
if (conn->sock < 0)
|
||||
{
|
||||
strcpy(conn->errorMessage, "pqReadData() -- connection not open\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Left-justify any data in the buffer to make room */
|
||||
if (conn->inStart < conn->inEnd)
|
||||
{
|
||||
memmove(conn->inBuffer, conn->inBuffer + conn->inStart,
|
||||
conn->inEnd - conn->inStart);
|
||||
conn->inEnd -= conn->inStart;
|
||||
conn->inCursor -= conn->inStart;
|
||||
conn->inStart = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
conn->inStart = conn->inCursor = conn->inEnd = 0;
|
||||
}
|
||||
/* If the buffer is fairly full, enlarge it.
|
||||
* We need to be able to enlarge the buffer in case a single message
|
||||
* exceeds the initial buffer size. We enlarge before filling the
|
||||
* buffer entirely so as to avoid asking the kernel for a partial packet.
|
||||
* The magic constant here should be at least one TCP packet.
|
||||
*/
|
||||
if (conn->inBufSize - conn->inEnd < 2000)
|
||||
{
|
||||
int newSize = conn->inBufSize * 2;
|
||||
char * newBuf = (char *) realloc(conn->inBuffer, newSize);
|
||||
if (newBuf)
|
||||
{
|
||||
conn->inBuffer = newBuf;
|
||||
conn->inBufSize = newSize;
|
||||
}
|
||||
}
|
||||
|
||||
/* OK, try to read some data */
|
||||
tryAgain:
|
||||
nread = recv(conn->sock, conn->inBuffer + conn->inEnd,
|
||||
conn->inBufSize - conn->inEnd, 0);
|
||||
if (nread < 0)
|
||||
{
|
||||
if (errno == EINTR)
|
||||
goto tryAgain;
|
||||
sprintf(conn->errorMessage,
|
||||
"pqReadData() -- read() failed: errno=%d\n%s\n",
|
||||
errno, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
if (nread > 0)
|
||||
{
|
||||
conn->inEnd += nread;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* A return value of 0 could mean just that no data is now available,
|
||||
* or it could mean EOF --- that is, the server has closed the connection.
|
||||
* Since we have the socket in nonblock mode, the only way to tell the
|
||||
* difference is to see if select() is saying that the file is ready.
|
||||
* Grumble. Fortunately, we don't expect this path to be taken much,
|
||||
* since in normal practice we should not be trying to read data unless
|
||||
* the file selected for reading already.
|
||||
*/
|
||||
if (! pqReadReady(conn))
|
||||
return 0; /* definitely no data available */
|
||||
|
||||
/* Still not sure that it's EOF,
|
||||
* because some data could have just arrived.
|
||||
*/
|
||||
tryAgain2:
|
||||
nread = recv(conn->sock, conn->inBuffer + conn->inEnd,
|
||||
conn->inBufSize - conn->inEnd, 0);
|
||||
if (nread < 0)
|
||||
{
|
||||
if (errno == EINTR)
|
||||
goto tryAgain2;
|
||||
sprintf(conn->errorMessage,
|
||||
"pqReadData() -- read() failed: errno=%d\n%s\n",
|
||||
errno, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
if (nread > 0)
|
||||
{
|
||||
conn->inEnd += nread;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* OK, we are getting a zero read even though select() says ready.
|
||||
* This means the connection has been closed. Cope.
|
||||
*/
|
||||
sprintf(conn->errorMessage,
|
||||
"pqReadData() -- backend closed the channel unexpectedly.\n"
|
||||
"\tThis probably means the backend terminated abnormally"
|
||||
" before or while processing the request.\n");
|
||||
conn->status = CONNECTION_BAD; /* No more connection to
|
||||
* backend */
|
||||
close(conn->sock);
|
||||
conn->sock = -1;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pqFlush: send any data waiting in the output buffer
|
||||
*/
|
||||
int
|
||||
pqFlush(PGconn *conn)
|
||||
{
|
||||
char * ptr = conn->outBuffer;
|
||||
int len = conn->outCount;
|
||||
|
||||
if (conn->sock < 0)
|
||||
{
|
||||
strcpy(conn->errorMessage, "pqFlush() -- connection not open\n");
|
||||
return EOF;
|
||||
}
|
||||
|
||||
while (len > 0)
|
||||
{
|
||||
int sent = send(conn->sock, ptr, len, 0);
|
||||
if (sent < 0)
|
||||
{
|
||||
/* Anything except EAGAIN or EWOULDBLOCK is trouble */
|
||||
switch (errno)
|
||||
{
|
||||
#ifdef EAGAIN
|
||||
case EAGAIN:
|
||||
break;
|
||||
#endif
|
||||
#if defined(EWOULDBLOCK) && (!defined(EAGAIN) || (EWOULDBLOCK != EAGAIN))
|
||||
case EWOULDBLOCK:
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
sprintf(conn->errorMessage,
|
||||
"pqFlush() -- couldn't send data: errno=%d\n%s\n",
|
||||
errno, strerror(errno));
|
||||
return EOF;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ptr += sent;
|
||||
len -= sent;
|
||||
}
|
||||
if (len > 0)
|
||||
{
|
||||
/* We didn't send it all, wait till we can send more */
|
||||
if (pqWait(FALSE, TRUE, conn))
|
||||
return EOF;
|
||||
}
|
||||
}
|
||||
|
||||
conn->outCount = 0;
|
||||
|
||||
if (conn->Pfdebug)
|
||||
fflush(conn->Pfdebug);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* pqWait: wait until we can read or write the connection socket
|
||||
*/
|
||||
int
|
||||
pqWait(int forRead, int forWrite, PGconn *conn)
|
||||
{
|
||||
fd_set input_mask;
|
||||
fd_set output_mask;
|
||||
|
||||
if (conn->sock < 0)
|
||||
{
|
||||
strcpy(conn->errorMessage, "pqWait() -- connection not open\n");
|
||||
return EOF;
|
||||
}
|
||||
|
||||
/* loop in case select returns EINTR */
|
||||
for (;;) {
|
||||
FD_ZERO(&input_mask);
|
||||
FD_ZERO(&output_mask);
|
||||
if (forRead)
|
||||
FD_SET(conn->sock, &input_mask);
|
||||
if (forWrite)
|
||||
FD_SET(conn->sock, &output_mask);
|
||||
if (select(conn->sock+1, &input_mask, &output_mask, (fd_set *) NULL,
|
||||
(struct timeval *) NULL) < 0)
|
||||
{
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
sprintf(conn->errorMessage,
|
||||
"pqWait() -- select() failed: errno=%d\n%s\n",
|
||||
errno, strerror(errno));
|
||||
return EOF;
|
||||
}
|
||||
/* On nonerror return, assume we're done */
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
Reference in New Issue
Block a user