Commit 4144b5b8 authored by Alexandre Julliard's avatar Alexandre Julliard

Create the server directory and socket file in /tmp.

Use fcntl file locking to ensure exclusion on the server socket and to better recover from crashes. Flush the registry before closing the socket to avoid timeouts on the client side. Moved get_config_dir functionality to libwine.
parent 88e42619
......@@ -56,16 +56,17 @@
#include "ntddk.h"
#include "wine/winbase16.h" /* for GetCurrentTask */
#include "winerror.h"
#include "winioctl.h"
#include "ntddstor.h"
#include "ntddcdrm.h"
#include "drive.h"
#include "file.h"
#include "heap.h"
#include "msdos.h"
#include "task.h"
#include "wine/debug.h"
#include "wine/library.h"
#include "wine/server.h"
#include "winioctl.h"
#include "ntddstor.h"
#include "ntddcdrm.h"
#include "wine/debug.h"
WINE_DEFAULT_DEBUG_CHANNEL(dosfs);
WINE_DECLARE_DEBUG_CHANNEL(file);
......@@ -197,7 +198,7 @@ int DRIVE_Init(void)
else
{
/* relative paths are relative to config dir */
const char *config = get_config_dir();
const char *config = wine_get_config_dir();
drive->root = HeapAlloc( GetProcessHeap(), 0, strlen(config) + strlen(path) + 2 );
sprintf( drive->root, "%s/%s", config, path );
}
......
......@@ -38,8 +38,9 @@
#include "winreg.h"
#include "file.h"
#include "heap.h"
#include "wine/debug.h"
#include "wine/server.h"
#include "wine/library.h"
#include "wine/debug.h"
WINE_DEFAULT_DEBUG_CHANNEL(profile);
......@@ -544,7 +545,7 @@ static BOOL PROFILE_FlushFile(void)
{
/* Try to create it in $HOME/.wine */
/* FIXME: this will need a more general solution */
strcpy( buffer, get_config_dir() );
strcpy( buffer, wine_get_config_dir() );
p = buffer + strlen(buffer);
*p++ = '/';
strcpy( p, strrchr( CurProfile->dos_name, '\\' ) + 1 );
......@@ -681,7 +682,7 @@ static BOOL PROFILE_Open( LPCSTR filename )
/* Try to open the profile file, first in $HOME/.wine */
/* FIXME: this will need a more general solution */
strcpy( buffer, get_config_dir() );
strcpy( buffer, wine_get_config_dir() );
p = buffer + strlen(buffer);
*p++ = '/';
strcpy( p, strrchr( newdos_name, '\\' ) + 1 );
......@@ -1046,7 +1047,7 @@ int PROFILE_LoadWineIni(void)
}
RtlFreeUnicodeString( &nameW );
if (!CLIENT_IsBootThread()) return 1; /* already loaded */
if (disp == REG_OPENED_EXISTING_KEY) return 1; /* loaded by the server */
if ((p = getenv( "HOME" )) != NULL)
{
......@@ -1055,36 +1056,23 @@ int PROFILE_LoadWineIni(void)
if ((f = fopen( buffer, "r" )) != NULL)
{
lstrcpynA(PROFILE_WineIniUsed,buffer,MAX_PATHNAME_LEN);
goto found;
/* convert to the new format */
sprintf( buffer, "%s/config", wine_get_config_dir() );
convert_config( f, buffer );
fclose( f );
MESSAGE( "The '%s' configuration file has been converted\n"
"to the new format and saved as '%s'.\n", PROFILE_WineIniUsed, buffer );
MESSAGE( "You should verify that the contents of the new file are correct,\n"
"and then remove the old one and restart Wine.\n" );
ExitProcess(0);
}
}
else WARN("could not get $HOME value for config file.\n" );
if (disp == REG_OPENED_EXISTING_KEY) return 1; /* loaded by the server */
MESSAGE( "Can't open configuration file %s/config\n",get_config_dir() );
MESSAGE( "Can't open configuration file %s/config\n", wine_get_config_dir() );
return 0;
found:
if (disp == REG_OPENED_EXISTING_KEY)
{
MESSAGE( "Warning: configuration loaded by the server from '%s/config',\n"
" file '%s' was ignored.\n", get_config_dir(), PROFILE_WineIniUsed );
fclose( f );
return 1;
}
/* convert to the new format */
sprintf( buffer, "%s/config", get_config_dir() );
convert_config( f, buffer );
fclose( f );
MESSAGE( "The '%s' configuration file has been converted\n"
"to the new format and saved as '%s'.\n", PROFILE_WineIniUsed, buffer );
MESSAGE( "You should verify that the contents of the new file are correct,\n"
"and then remove the old one and restart Wine.\n" );
ExitProcess(0);
}
......@@ -1099,7 +1087,7 @@ void PROFILE_UsageWineIni(void)
{
MESSAGE("Perhaps you have not properly edited or created "
"your Wine configuration file.\n");
MESSAGE("This is (supposed to be) '%s/config'\n", get_config_dir());
MESSAGE("This is (supposed to be) '%s/config'\n", wine_get_config_dir());
/* RTFM, so to say */
}
......
......@@ -42,11 +42,11 @@
#include "winnls.h"
#include "winreg.h"
#include "font.h"
#include "wine/debug.h"
#include "user.h" /* for TWEAK_WineLook (FIXME) */
#include "x11font.h"
#include "wine/server.h"
#include "wine/library.h"
#include "wine/unicode.h"
#include "wine/debug.h"
WINE_DEFAULT_DEBUG_CHANNEL(font);
......@@ -1848,7 +1848,7 @@ static void XFONT_LoadIgnores(void)
*/
static char* XFONT_UserMetricsCache( char* buffer, int* buf_size )
{
const char *confdir = get_config_dir();
const char *confdir = wine_get_config_dir();
const char *display_name = XDisplayName(NULL);
int len = strlen(confdir) + strlen(INIFontMetrics) + strlen(display_name) + 8;
int display = 0;
......
......@@ -24,6 +24,11 @@
#include <sys/types.h>
#include "winbase.h"
/* configuration */
extern const char *wine_get_config_dir(void);
extern const char *wine_get_server_dir(void);
/* dll loading */
typedef void (*load_dll_callback_t)( void *, const char * );
......
......@@ -113,7 +113,6 @@ inline static void wine_server_set_reply( void *req_ptr, void *ptr, unsigned int
/* non-exported functions */
extern void server_protocol_error( const char *err, ... ) WINE_NORETURN;
extern void server_protocol_perror( const char *err ) WINE_NORETURN;
extern const char *get_config_dir(void);
extern void CLIENT_InitServer(void);
extern void CLIENT_InitThread(void);
extern void CLIENT_BootDone( int debug_level );
......
......@@ -10,6 +10,7 @@ SONAME = libwine.so
EXTRALIBS = @DLLIBS@
C_SRCS = \
config.c \
debug.c \
errno.c \
ldt.c \
......
/*
* Configuration parameters shared between Wine server and clients
*
* Copyright 2002 Alexandre Julliard
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "config.h"
#include "wine/port.h"
#include <errno.h>
#include <pwd.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
static const char * const server_config_dir = "/.wine"; /* config dir relative to $HOME */
static const char * const server_root_prefix = "/tmp/.wine-"; /* prefix for server root dir */
static const char * const server_dir_prefix = "/server-"; /* prefix for server dir */
static char *config_dir;
static char *server_dir;
#ifdef __GNUC__
static void fatal_error( const char *err, ... ) __attribute__((noreturn,format(printf,1,2)));
static void fatal_perror( const char *err, ... ) __attribute__((noreturn,format(printf,1,2)));
#endif
/* die on a fatal error */
static void fatal_error( const char *err, ... )
{
va_list args;
va_start( args, err );
fprintf( stderr, "wine: " );
vfprintf( stderr, err, args );
va_end( args );
exit(1);
}
/* die on a fatal error */
static void fatal_perror( const char *err, ... )
{
va_list args;
va_start( args, err );
fprintf( stderr, "wine: " );
vfprintf( stderr, err, args );
perror( " " );
va_end( args );
exit(1);
}
/* malloc wrapper */
static void *xmalloc( size_t size )
{
void *res;
if (!size) size = 1;
if (!(res = malloc( size ))) fatal_error( "virtual memory exhausted\n");
return res;
}
/* remove all trailing slashes from a path name */
inline static void remove_trailing_slashes( char *path )
{
int len = strlen( path );
while (len > 1 && path[len-1] == '/') path[--len] = 0;
}
/* initialize all the paths values */
static void init_paths(void)
{
struct stat st;
char uid_str[32], *p;
const char *home = getenv( "HOME" );
const char *user = NULL;
const char *prefix = getenv( "WINEPREFIX" );
struct passwd *pwd = getpwuid( getuid() );
if (pwd)
{
user = pwd->pw_name;
if (!home) home = pwd->pw_dir;
}
if (!user)
{
sprintf( uid_str, "%d", getuid() );
user = uid_str;
}
/* build config_dir */
if (prefix)
{
if (!(config_dir = strdup( prefix ))) fatal_error( "virtual memory exhausted\n");
remove_trailing_slashes( config_dir );
if (config_dir[0] != '/')
fatal_error( "invalid directory %s in WINEPREFIX: not an absolute path\n", prefix );
if (stat( config_dir, &st ) == -1)
fatal_perror( "cannot open %s as specified in WINEPREFIX", config_dir );
}
else
{
if (!home) fatal_error( "could not determine your home directory\n" );
if (home[0] != '/') fatal_error( "your home directory %s is not an absolute path\n", home );
config_dir = xmalloc( strlen(home) + strlen(server_config_dir) + 1 );
strcpy( config_dir, home );
remove_trailing_slashes( config_dir );
strcat( config_dir, server_config_dir );
if (stat( config_dir, &st ) == -1)
fatal_perror( "cannot open %s", config_dir );
}
if (!S_ISDIR(st.st_mode)) fatal_error( "%s is not a directory\n", config_dir );
/* build server_dir */
server_dir = xmalloc( strlen(server_root_prefix) + strlen(user) + strlen( server_dir_prefix ) +
2*sizeof(st.st_dev) + 2*sizeof(st.st_ino) + 2 );
strcpy( server_dir, server_root_prefix );
p = server_dir + strlen(server_dir);
strcpy( p, user );
while (*p)
{
if (*p == '/') *p = '!';
p++;
}
strcpy( p, server_dir_prefix );
if (sizeof(st.st_dev) > sizeof(unsigned long))
sprintf( server_dir + strlen(server_dir), "%llx-", (unsigned long long)st.st_dev );
else
sprintf( server_dir + strlen(server_dir), "%lx-", (unsigned long)st.st_dev );
if (sizeof(st.st_ino) > sizeof(unsigned long))
sprintf( server_dir + strlen(server_dir), "%llx", (unsigned long long)st.st_ino );
else
sprintf( server_dir + strlen(server_dir), "%lx", (unsigned long)st.st_ino );
}
/* return the configuration directory ($WINEPREFIX or $HOME/.wine) */
const char *wine_get_config_dir(void)
{
if (!config_dir) init_paths();
return config_dir;
}
/* return the full name of the server directory (the one containing the socket) */
const char *wine_get_server_dir(void)
{
if (!server_dir) init_paths();
return server_dir;
}
......@@ -53,6 +53,7 @@
#include "winreg.h"
#include "wine/winbase16.h"
#include "wine/library.h"
#include "wine/server.h"
#include "wine/unicode.h"
#include "file.h"
......@@ -1030,7 +1031,7 @@ static void _set_registry_levels(int level,int saving,int period)
/* _save_at_exit [Internal] */
static void _save_at_exit(HKEY hkey,LPCSTR path)
{
LPCSTR confdir = get_config_dir();
LPCSTR confdir = wine_get_config_dir();
SERVER_START_REQ( save_registry_atexit )
{
......@@ -1547,7 +1548,7 @@ static void _load_global_registry(void)
/* load home registry files (stored in ~/.wine) [Internal] */
static void _load_home_registry( HKEY hkey_users_default )
{
LPCSTR confdir = get_config_dir();
LPCSTR confdir = wine_get_config_dir();
LPSTR tmp = _xmalloc(strlen(confdir)+20);
strcpy(tmp,confdir);
......
......@@ -46,6 +46,7 @@
#include <stdarg.h>
#include "thread.h"
#include "wine/library.h"
#include "wine/server.h"
#include "winerror.h"
#include "options.h"
......@@ -55,9 +56,8 @@
#define SCM_RIGHTS 1
#endif
#define CONFDIR "/.wine" /* directory for Wine config relative to $HOME */
#define SERVERDIR "/wineserver-" /* server socket directory (hostname appended) */
#define SOCKETNAME "socket" /* name of the socket file */
#define LOCKNAME "lock" /* name of the lock file */
#ifndef HAVE_MSGHDR_ACCRIGHTS
/* data structure used to pass an fd with sendmsg/recvmsg */
......@@ -74,8 +74,13 @@ static void *boot_thread_id;
static sigset_t block_set; /* signals to block during server calls */
static int fd_socket; /* socket to exchange file descriptors with the server */
#ifdef __GNUC__
static void fatal_error( const char *err, ... ) __attribute__((noreturn, format(printf,1,2)));
static void fatal_perror( const char *err, ... ) __attribute__((noreturn, format(printf,1,2)));
static void server_connect_error( const char *serverdir ) __attribute__((noreturn));
#endif
/* die on a fatal error; use only during initialization */
static void fatal_error( const char *err, ... ) WINE_NORETURN;
static void fatal_error( const char *err, ... )
{
va_list args;
......@@ -88,7 +93,6 @@ static void fatal_error( const char *err, ... )
}
/* die on a fatal error; use only during initialization */
static void fatal_perror( const char *err, ... ) WINE_NORETURN;
static void fatal_perror( const char *err, ... )
{
va_list args;
......@@ -434,42 +438,6 @@ int wine_server_handle_to_fd( obj_handle_t handle, unsigned int access, int *uni
/***********************************************************************
* get_config_dir
*
* Return the configuration directory ($WINEPREFIX or $HOME/.wine)
*/
const char *get_config_dir(void)
{
static char *confdir;
if (!confdir)
{
const char *prefix = getenv( "WINEPREFIX" );
if (prefix)
{
int len = strlen(prefix);
if (!(confdir = strdup( prefix ))) fatal_error( "out of memory\n" );
if (len > 1 && confdir[len-1] == '/') confdir[len-1] = 0;
}
else
{
const char *home = getenv( "HOME" );
if (!home)
{
struct passwd *pwd = getpwuid( getuid() );
if (!pwd) fatal_error( "could not find your home directory\n" );
home = pwd->pw_dir;
}
if (!(confdir = malloc( strlen(home) + strlen(CONFDIR) + 1 )))
fatal_error( "out of memory\n" );
strcpy( confdir, home );
strcat( confdir, CONFDIR );
}
}
return confdir;
}
/***********************************************************************
* start_server
*
* Start a new wine server.
......@@ -495,7 +463,7 @@ static void start_server( const char *oldcwd )
sprintf( path, "%s/%s", oldcwd, p );
p = path;
}
execl( p, "wineserver", NULL );
execl( p, p, NULL );
fatal_perror( "could not exec the server '%s'\n"
" specified in the WINESERVER environment variable", p );
}
......@@ -511,7 +479,7 @@ static void start_server( const char *oldcwd )
if ((p = strrchr( strcpy( path, full_argv0 ), '/' )))
{
strcpy( p, "/wineserver" );
execl( path, "wineserver", NULL );
execl( path, path, NULL );
}
free(path);
}
......@@ -520,13 +488,48 @@ static void start_server( const char *oldcwd )
execlp( "wineserver", "wineserver", NULL );
fatal_error( "could not exec wineserver\n" );
}
started = 1;
waitpid( pid, &status, 0 );
status = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
if (status == 2) return; /* server lock held by someone else, will retry later */
if (status) exit(status); /* server failed */
started = 1;
}
}
/***********************************************************************
* server_connect_error
*
* Try to display a meaningful explanation of why we couldn't connect
* to the server.
*/
static void server_connect_error( const char *serverdir )
{
int fd;
struct flock fl;
if ((fd = open( LOCKNAME, O_WRONLY )) == -1)
fatal_error( "for some mysterious reason, the wine server never started.\n" );
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 1;
if (fcntl( fd, F_GETLK, &fl ) != -1)
{
if (fl.l_type == F_WRLCK) /* the file is locked */
fatal_error( "a wine server seems to be running, but I cannot connect to it.\n"
" You probably need to kill that process (it might be pid %d).\n",
(int)fl.l_pid );
fatal_error( "for some mysterious reason, the wine server failed to run.\n" );
}
fatal_error( "the file system of '%s' doesn't support locks,\n"
" and there is a 'socket' file in that directory that prevents wine from starting.\n"
" You should make sure no wine server is running, remove that file and try again.\n",
serverdir );
}
/***********************************************************************
* server_connect
*
......@@ -552,17 +555,20 @@ static int server_connect( const char *oldcwd, const char *serverdir )
if (st.st_uid != getuid()) fatal_error( "'%s' is not owned by you\n", serverdir );
if (st.st_mode & 077) fatal_error( "'%s' must not be accessible by other users\n", serverdir );
for (retry = 0; retry < 3; retry++)
for (retry = 0; retry < 6; retry++)
{
/* if not the first try, wait a bit to leave the server time to exit */
if (retry) usleep( 100000 * retry * retry );
/* check for an existing socket */
if (lstat( SOCKETNAME, &st ) == -1)
/* if not the first try, wait a bit to leave the previous server time to exit */
if (retry)
{
usleep( 100000 * retry * retry );
start_server( oldcwd );
if (lstat( SOCKETNAME, &st ) == -1) continue; /* still no socket, wait a bit more */
}
else if (lstat( SOCKETNAME, &st ) == -1) /* check for an already existing socket */
{
if (errno != ENOENT) fatal_perror( "lstat %s/%s", serverdir, SOCKETNAME );
start_server( oldcwd );
if (lstat( SOCKETNAME, &st ) == -1) fatal_perror( "lstat %s/%s", serverdir, SOCKETNAME );
if (lstat( SOCKETNAME, &st ) == -1) continue; /* still no socket, wait a bit more */
}
/* make sure the socket is sane (ISFIFO needed for Solaris) */
......@@ -586,10 +592,7 @@ static int server_connect( const char *oldcwd, const char *serverdir )
}
close( s );
}
fatal_error( "file '%s/%s' exists,\n"
" but I cannot connect to it; maybe the wineserver has crashed?\n"
" If this is the case, you should remove this socket file and try again.\n",
serverdir, SOCKETNAME );
server_connect_error( serverdir );
}
......@@ -601,9 +604,7 @@ static int server_connect( const char *oldcwd, const char *serverdir )
void CLIENT_InitServer(void)
{
int size;
char hostname[64];
char *oldcwd, *serverdir;
const char *configdir;
char *oldcwd;
obj_handle_t dummy_handle;
/* retrieve the current directory */
......@@ -631,17 +632,8 @@ void CLIENT_InitServer(void)
}
}
/* get the server directory name */
if (gethostname( hostname, sizeof(hostname) ) == -1) fatal_perror( "gethostname" );
configdir = get_config_dir();
serverdir = malloc( strlen(configdir) + strlen(SERVERDIR) + strlen(hostname) + 1 );
if (!serverdir) fatal_error( "out of memory\n" );
strcpy( serverdir, configdir );
strcat( serverdir, SERVERDIR );
strcat( serverdir, hostname );
/* connect to the server */
fd_socket = server_connect( oldcwd, serverdir );
fd_socket = server_connect( oldcwd, wine_get_server_dir() );
/* switch back to the starting directory */
if (oldcwd)
......
......@@ -34,16 +34,18 @@
/* command-line options */
int debug_level = 0;
int master_socket_timeout = 3; /* master socket timeout in seconds, default is 3 s */
const char *server_argv0;
/* parse-line args */
/* FIXME: should probably use getopt, and add a (more complete?) help option */
static void usage(const char *exeName)
static void usage(void)
{
fprintf(stderr, "\nusage: %s [options]\n\n", exeName);
fprintf(stderr, "\nusage: %s [options]\n\n", server_argv0);
fprintf(stderr, "options:\n");
fprintf(stderr, " -d<n> set debug level to <n>\n");
fprintf(stderr, " -p[n] make server persistent, optionally for n seconds\n");
fprintf(stderr, " -w wait until the current wineserver terminates\n");
fprintf(stderr, " -h display this help message\n");
fprintf(stderr, "\n");
}
......@@ -51,6 +53,8 @@ static void usage(const char *exeName)
static void parse_args( int argc, char *argv[] )
{
int i;
server_argv0 = argv[0];
for (i = 1; i < argc; i++)
{
if (argv[i][0] == '-')
......@@ -62,23 +66,26 @@ static void parse_args( int argc, char *argv[] )
else debug_level++;
break;
case 'h':
usage( argv[0] );
usage();
exit(0);
break;
case 'p':
if (isdigit(argv[i][2])) master_socket_timeout = atoi( argv[i] + 2 );
else master_socket_timeout = -1;
break;
case 'w':
wait_for_lock();
exit(0);
default:
fprintf( stderr, "Unknown option '%s'\n", argv[i] );
usage( argv[0] );
fprintf( stderr, "wineserver: unknown option '%s'\n", argv[i] );
usage();
exit(1);
}
}
else
{
fprintf( stderr, "Unknown argument '%s'. Your version of wine may be too old.\n", argv[i] );
usage(argv[0]);
fprintf( stderr, "wineserver: unknown argument '%s'.\n", argv[i] );
usage();
exit(1);
}
}
......@@ -107,16 +114,12 @@ int main( int argc, char *argv[] )
open_master_socket();
setvbuf( stderr, NULL, _IOLBF, 0 );
if (debug_level) fprintf( stderr, "Server: starting (pid=%ld)\n", (long) getpid() );
if (debug_level) fprintf( stderr, "wineserver: starting (pid=%ld)\n", (long) getpid() );
init_registry();
select_loop();
close_registry();
if (debug_level) fprintf( stderr, "Server: exiting (pid=%ld)\n", (long) getpid() );
#ifdef DEBUG_OBJECTS
close_atom_table();
dump_objects(); /* dump any remaining objects */
#endif
return 0;
}
......@@ -191,7 +191,7 @@ struct thread *create_process( int fd )
struct thread *thread = NULL;
int request_pipe[2];
if (!(process = alloc_object( &process_ops, fd ))) return NULL;
if (!(process = alloc_object( &process_ops, fd ))) goto error;
process->next = NULL;
process->prev = NULL;
process->parent = NULL;
......@@ -230,7 +230,12 @@ struct thread *create_process( int fd )
file_set_error();
goto error;
}
send_client_fd( process, request_pipe[1], 0 );
if (send_client_fd( process, request_pipe[1], 0 ) == -1)
{
close( request_pipe[0] );
close( request_pipe[1] );
goto error;
}
close( request_pipe[1] );
if (!(thread = create_thread( request_pipe[0], process ))) goto error;
......@@ -239,8 +244,9 @@ struct thread *create_process( int fd )
return thread;
error:
if (thread) release_object( thread );
release_object( process );
if (process) release_object( process );
/* if we failed to start our first process, close everything down */
if (!running_processes) close_master_socket();
return NULL;
}
......@@ -502,13 +508,7 @@ static void process_killed( struct process *process )
if (process->exe.file) release_object( process->exe.file );
process->exe.file = NULL;
wake_up( &process->obj, 0 );
if (!--running_processes)
{
/* last process died, close global handles */
close_global_handles();
/* this will cause the select loop to terminate */
close_master_socket();
}
if (!--running_processes) close_master_socket();
}
/* add a thread to a process running threads list */
......
......@@ -59,6 +59,7 @@ extern unsigned int get_tick_count(void);
extern void open_master_socket(void);
extern void close_master_socket(void);
extern void lock_master_socket( int locked );
extern int wait_for_lock(void);
extern void trace_request(void);
extern void trace_reply( enum request req, const union generic_reply *reply );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment