6819213: revive sun.boot.library.path
Support multiplex and mutable sun.boot.library.path Reviewed-by: acorn, dcubed, xlu
This commit is contained in:
parent
d37d544754
commit
4be7c3c672
@ -1518,21 +1518,51 @@ const char* os::dll_file_extension() { return ".so"; }
|
||||
|
||||
const char* os::get_temp_directory() { return "/tmp/"; }
|
||||
|
||||
void os::dll_build_name(
|
||||
char* buffer, size_t buflen, const char* pname, const char* fname) {
|
||||
// copied from libhpi
|
||||
static bool file_exists(const char* filename) {
|
||||
struct stat statbuf;
|
||||
if (filename == NULL || strlen(filename) == 0) {
|
||||
return false;
|
||||
}
|
||||
return os::stat(filename, &statbuf) == 0;
|
||||
}
|
||||
|
||||
void os::dll_build_name(char* buffer, size_t buflen,
|
||||
const char* pname, const char* fname) {
|
||||
// Copied from libhpi
|
||||
const size_t pnamelen = pname ? strlen(pname) : 0;
|
||||
|
||||
/* Quietly truncate on buffer overflow. Should be an error. */
|
||||
// Quietly truncate on buffer overflow. Should be an error.
|
||||
if (pnamelen + strlen(fname) + 10 > (size_t) buflen) {
|
||||
*buffer = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (pnamelen == 0) {
|
||||
sprintf(buffer, "lib%s.so", fname);
|
||||
snprintf(buffer, buflen, "lib%s.so", fname);
|
||||
} else if (strchr(pname, *os::path_separator()) != NULL) {
|
||||
int n;
|
||||
char** pelements = split_path(pname, &n);
|
||||
for (int i = 0 ; i < n ; i++) {
|
||||
// Really shouldn't be NULL, but check can't hurt
|
||||
if (pelements[i] == NULL || strlen(pelements[i]) == 0) {
|
||||
continue; // skip the empty path values
|
||||
}
|
||||
snprintf(buffer, buflen, "%s/lib%s.so", pelements[i], fname);
|
||||
if (file_exists(buffer)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// release the storage
|
||||
for (int i = 0 ; i < n ; i++) {
|
||||
if (pelements[i] != NULL) {
|
||||
FREE_C_HEAP_ARRAY(char, pelements[i]);
|
||||
}
|
||||
}
|
||||
if (pelements != NULL) {
|
||||
FREE_C_HEAP_ARRAY(char*, pelements);
|
||||
}
|
||||
} else {
|
||||
sprintf(buffer, "%s/lib%s.so", pname, fname);
|
||||
snprintf(buffer, buflen, "%s/lib%s.so", pname, fname);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1827,21 +1827,51 @@ const char* os::dll_file_extension() { return ".so"; }
|
||||
|
||||
const char* os::get_temp_directory() { return "/tmp/"; }
|
||||
|
||||
void os::dll_build_name(
|
||||
char* buffer, size_t buflen, const char* pname, const char* fname) {
|
||||
// copied from libhpi
|
||||
static bool file_exists(const char* filename) {
|
||||
struct stat statbuf;
|
||||
if (filename == NULL || strlen(filename) == 0) {
|
||||
return false;
|
||||
}
|
||||
return os::stat(filename, &statbuf) == 0;
|
||||
}
|
||||
|
||||
void os::dll_build_name(char* buffer, size_t buflen,
|
||||
const char* pname, const char* fname) {
|
||||
// Copied from libhpi
|
||||
const size_t pnamelen = pname ? strlen(pname) : 0;
|
||||
|
||||
/* Quietly truncate on buffer overflow. Should be an error. */
|
||||
// Quietly truncate on buffer overflow. Should be an error.
|
||||
if (pnamelen + strlen(fname) + 10 > (size_t) buflen) {
|
||||
*buffer = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (pnamelen == 0) {
|
||||
sprintf(buffer, "lib%s.so", fname);
|
||||
snprintf(buffer, buflen, "lib%s.so", fname);
|
||||
} else if (strchr(pname, *os::path_separator()) != NULL) {
|
||||
int n;
|
||||
char** pelements = split_path(pname, &n);
|
||||
for (int i = 0 ; i < n ; i++) {
|
||||
// really shouldn't be NULL but what the heck, check can't hurt
|
||||
if (pelements[i] == NULL || strlen(pelements[i]) == 0) {
|
||||
continue; // skip the empty path values
|
||||
}
|
||||
snprintf(buffer, buflen, "%s/lib%s.so", pelements[i], fname);
|
||||
if (file_exists(buffer)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// release the storage
|
||||
for (int i = 0 ; i < n ; i++) {
|
||||
if (pelements[i] != NULL) {
|
||||
FREE_C_HEAP_ARRAY(char, pelements[i]);
|
||||
}
|
||||
}
|
||||
if (pelements != NULL) {
|
||||
FREE_C_HEAP_ARRAY(char*, pelements);
|
||||
}
|
||||
} else {
|
||||
sprintf(buffer, "%s/lib%s.so", pname, fname);
|
||||
snprintf(buffer, buflen, "%s/lib%s.so", pname, fname);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1004,26 +1004,61 @@ const char * os::get_temp_directory()
|
||||
}
|
||||
}
|
||||
|
||||
void os::dll_build_name(char *holder, size_t holderlen,
|
||||
const char* pname, const char* fname)
|
||||
{
|
||||
// copied from libhpi
|
||||
const size_t pnamelen = pname ? strlen(pname) : 0;
|
||||
const char c = (pnamelen > 0) ? pname[pnamelen-1] : 0;
|
||||
static bool file_exists(const char* filename) {
|
||||
if (filename == NULL || strlen(filename) == 0) {
|
||||
return false;
|
||||
}
|
||||
return GetFileAttributes(filename) != INVALID_FILE_ATTRIBUTES;
|
||||
}
|
||||
|
||||
/* Quietly truncates on buffer overflow. Should be an error. */
|
||||
if (pnamelen + strlen(fname) + 10 > holderlen) {
|
||||
*holder = '\0';
|
||||
return;
|
||||
}
|
||||
void os::dll_build_name(char *buffer, size_t buflen,
|
||||
const char* pname, const char* fname) {
|
||||
// Copied from libhpi
|
||||
const size_t pnamelen = pname ? strlen(pname) : 0;
|
||||
const char c = (pnamelen > 0) ? pname[pnamelen-1] : 0;
|
||||
|
||||
if (pnamelen == 0) {
|
||||
sprintf(holder, "%s.dll", fname);
|
||||
} else if (c == ':' || c == '\\') {
|
||||
sprintf(holder, "%s%s.dll", pname, fname);
|
||||
} else {
|
||||
sprintf(holder, "%s\\%s.dll", pname, fname);
|
||||
// Quietly truncates on buffer overflow. Should be an error.
|
||||
if (pnamelen + strlen(fname) + 10 > buflen) {
|
||||
*buffer = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (pnamelen == 0) {
|
||||
jio_snprintf(buffer, buflen, "%s.dll", fname);
|
||||
} else if (c == ':' || c == '\\') {
|
||||
jio_snprintf(buffer, buflen, "%s%s.dll", pname, fname);
|
||||
} else if (strchr(pname, *os::path_separator()) != NULL) {
|
||||
int n;
|
||||
char** pelements = split_path(pname, &n);
|
||||
for (int i = 0 ; i < n ; i++) {
|
||||
char* path = pelements[i];
|
||||
// Really shouldn't be NULL, but check can't hurt
|
||||
size_t plen = (path == NULL) ? 0 : strlen(path);
|
||||
if (plen == 0) {
|
||||
continue; // skip the empty path values
|
||||
}
|
||||
const char lastchar = path[plen - 1];
|
||||
if (lastchar == ':' || lastchar == '\\') {
|
||||
jio_snprintf(buffer, buflen, "%s%s.dll", path, fname);
|
||||
} else {
|
||||
jio_snprintf(buffer, buflen, "%s\\%s.dll", path, fname);
|
||||
}
|
||||
if (file_exists(buffer)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// release the storage
|
||||
for (int i = 0 ; i < n ; i++) {
|
||||
if (pelements[i] != NULL) {
|
||||
FREE_C_HEAP_ARRAY(char, pelements[i]);
|
||||
}
|
||||
}
|
||||
if (pelements != NULL) {
|
||||
FREE_C_HEAP_ARRAY(char*, pelements);
|
||||
}
|
||||
} else {
|
||||
jio_snprintf(buffer, buflen, "%s\\%s.dll", pname, fname);
|
||||
}
|
||||
}
|
||||
|
||||
// Needs to be in os specific directory because windows requires another
|
||||
|
@ -852,16 +852,13 @@ bool Arguments::add_property(const char* prop) {
|
||||
FreeHeap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (strcmp(key, "sun.java.command") == 0) {
|
||||
|
||||
} else if (strcmp(key, "sun.java.command") == 0) {
|
||||
_java_command = value;
|
||||
|
||||
// don't add this property to the properties exposed to the java application
|
||||
FreeHeap(key);
|
||||
return true;
|
||||
}
|
||||
else if (strcmp(key, "sun.java.launcher.pid") == 0) {
|
||||
} else if (strcmp(key, "sun.java.launcher.pid") == 0) {
|
||||
// launcher.pid property is private and is processed
|
||||
// in process_sun_java_launcher_properties();
|
||||
// the sun.java.launcher property is passed on to the java application
|
||||
@ -870,13 +867,14 @@ bool Arguments::add_property(const char* prop) {
|
||||
FreeHeap(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (strcmp(key, "java.vendor.url.bug") == 0) {
|
||||
} else if (strcmp(key, "java.vendor.url.bug") == 0) {
|
||||
// save it in _java_vendor_url_bug, so JVM fatal error handler can access
|
||||
// its value without going through the property list or making a Java call.
|
||||
_java_vendor_url_bug = value;
|
||||
} else if (strcmp(key, "sun.boot.library.path") == 0) {
|
||||
PropertyList_unique_add(&_system_properties, key, value, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create new property and add at the end of the list
|
||||
PropertyList_unique_add(&_system_properties, key, value);
|
||||
return true;
|
||||
@ -895,7 +893,7 @@ void Arguments::set_mode_flags(Mode mode) {
|
||||
// Ensure Agent_OnLoad has the correct initial values.
|
||||
// This may not be the final mode; mode may change later in onload phase.
|
||||
PropertyList_unique_add(&_system_properties, "java.vm.info",
|
||||
(char*)Abstract_VM_Version::vm_info_string());
|
||||
(char*)Abstract_VM_Version::vm_info_string(), false);
|
||||
|
||||
UseInterpreter = true;
|
||||
UseCompiler = true;
|
||||
@ -2767,7 +2765,7 @@ void Arguments::PropertyList_add(SystemProperty** plist, const char* k, char* v)
|
||||
}
|
||||
|
||||
// This add maintains unique property key in the list.
|
||||
void Arguments::PropertyList_unique_add(SystemProperty** plist, const char* k, char* v) {
|
||||
void Arguments::PropertyList_unique_add(SystemProperty** plist, const char* k, char* v, jboolean append) {
|
||||
if (plist == NULL)
|
||||
return;
|
||||
|
||||
@ -2775,7 +2773,11 @@ void Arguments::PropertyList_unique_add(SystemProperty** plist, const char* k, c
|
||||
SystemProperty* prop;
|
||||
for (prop = *plist; prop != NULL; prop = prop->next()) {
|
||||
if (strcmp(k, prop->key()) == 0) {
|
||||
prop->set_value(v);
|
||||
if (append) {
|
||||
prop->append_value(v);
|
||||
} else {
|
||||
prop->set_value(v);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -475,10 +475,13 @@ class Arguments : AllStatic {
|
||||
// System properties
|
||||
static void init_system_properties();
|
||||
|
||||
// Proptery List manipulation
|
||||
// Property List manipulation
|
||||
static void PropertyList_add(SystemProperty** plist, SystemProperty *element);
|
||||
static void PropertyList_add(SystemProperty** plist, const char* k, char* v);
|
||||
static void PropertyList_unique_add(SystemProperty** plist, const char* k, char* v);
|
||||
static void PropertyList_unique_add(SystemProperty** plist, const char* k, char* v) {
|
||||
PropertyList_unique_add(plist, k, v, false);
|
||||
}
|
||||
static void PropertyList_unique_add(SystemProperty** plist, const char* k, char* v, jboolean append);
|
||||
static const char* PropertyList_get_value(SystemProperty* plist, const char* key);
|
||||
static int PropertyList_count(SystemProperty* pl);
|
||||
static const char* PropertyList_get_key_at(SystemProperty* pl,int index);
|
||||
|
@ -90,7 +90,7 @@ public:
|
||||
static inline struct protoent* get_proto_by_name(char* name);
|
||||
|
||||
// HPI_LibraryInterface
|
||||
static inline void dll_build_name(char *buf, int buf_len, char* path,
|
||||
static inline void dll_build_name(char *buf, int buf_len, const char* path,
|
||||
const char *name);
|
||||
static inline void* dll_load(const char *name, char *ebuf, int ebuflen);
|
||||
static inline void dll_unload(void *lib);
|
||||
@ -137,7 +137,15 @@ public:
|
||||
return result; \
|
||||
}
|
||||
|
||||
|
||||
#define VM_HPIDECL_VOID(name, names, func, arg_type, arg_print, arg) \
|
||||
inline void hpi::name arg_type { \
|
||||
if (TraceHPI) { \
|
||||
tty->print("hpi::" names "("); \
|
||||
tty->print arg_print; \
|
||||
tty->print(") = "); \
|
||||
} \
|
||||
func arg; \
|
||||
}
|
||||
|
||||
#define HPIDECL_VOID(name, names, intf, func, arg_type, arg_print, arg) \
|
||||
inline void hpi::name arg_type { \
|
||||
@ -197,11 +205,11 @@ HPIDECL(fsize, "fsize", _file, FileSizeFD, int, "%d",
|
||||
(fd, size));
|
||||
|
||||
// HPI_LibraryInterface
|
||||
HPIDECL_VOID(dll_build_name, "dll_build_name", _library, BuildLibName,
|
||||
(char *buf, int buf_len, char *path, const char *name),
|
||||
("buf = %p, buflen = %d, path = %s, name = %s",
|
||||
buf, buf_len, path, name),
|
||||
(buf, buf_len, path, name));
|
||||
VM_HPIDECL_VOID(dll_build_name, "dll_build_name", os::dll_build_name,
|
||||
(char *buf, int buf_len, const char *path, const char *name),
|
||||
("buf = %p, buflen = %d, path = %s, name = %s",
|
||||
buf, buf_len, path, name),
|
||||
(buf, buf_len, path, name));
|
||||
|
||||
VM_HPIDECL(dll_load, "dll_load", os::dll_load,
|
||||
void *, "(void *)%p",
|
||||
|
@ -863,7 +863,6 @@ char* os::format_boot_path(const char* format_string,
|
||||
|
||||
|
||||
bool os::set_boot_path(char fileSep, char pathSep) {
|
||||
|
||||
const char* home = Arguments::get_java_home();
|
||||
int home_len = (int)strlen(home);
|
||||
|
||||
@ -893,6 +892,60 @@ bool os::set_boot_path(char fileSep, char pathSep) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Splits a path, based on its separator, the number of
|
||||
* elements is returned back in n.
|
||||
* It is the callers responsibility to:
|
||||
* a> check the value of n, and n may be 0.
|
||||
* b> ignore any empty path elements
|
||||
* c> free up the data.
|
||||
*/
|
||||
char** os::split_path(const char* path, int* n) {
|
||||
*n = 0;
|
||||
if (path == NULL || strlen(path) == 0) {
|
||||
return NULL;
|
||||
}
|
||||
const char psepchar = *os::path_separator();
|
||||
char* inpath = (char*)NEW_C_HEAP_ARRAY(char, strlen(path) + 1);
|
||||
if (inpath == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
strncpy(inpath, path, strlen(path));
|
||||
int count = 1;
|
||||
char* p = strchr(inpath, psepchar);
|
||||
// Get a count of elements to allocate memory
|
||||
while (p != NULL) {
|
||||
count++;
|
||||
p++;
|
||||
p = strchr(p, psepchar);
|
||||
}
|
||||
char** opath = (char**) NEW_C_HEAP_ARRAY(char*, count);
|
||||
if (opath == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// do the actual splitting
|
||||
p = inpath;
|
||||
for (int i = 0 ; i < count ; i++) {
|
||||
size_t len = strcspn(p, os::path_separator());
|
||||
if (len > JVM_MAXPATHLEN) {
|
||||
return NULL;
|
||||
}
|
||||
// allocate the string and add terminator storage
|
||||
char* s = (char*)NEW_C_HEAP_ARRAY(char, len + 1);
|
||||
if (s == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
strncpy(s, p, len);
|
||||
s[len] = '\0';
|
||||
opath[i] = s;
|
||||
p += len + 1;
|
||||
}
|
||||
FREE_C_HEAP_ARRAY(char, inpath);
|
||||
*n = count;
|
||||
return opath;
|
||||
}
|
||||
|
||||
void os::set_memory_serialize_page(address page) {
|
||||
int count = log2_intptr(sizeof(class JavaThread)) - log2_intptr(64);
|
||||
_mem_serialize_page = (volatile int32_t *)page;
|
||||
|
@ -607,6 +607,7 @@ class os: AllStatic {
|
||||
char fileSep,
|
||||
char pathSep);
|
||||
static bool set_boot_path(char fileSep, char pathSep);
|
||||
static char** split_path(const char* path, int* n);
|
||||
};
|
||||
|
||||
// Note that "PAUSE" is almost always used with synchronization
|
||||
|
133
hotspot/test/runtime/6819213/TestBootNativeLibraryPath.java
Normal file
133
hotspot/test/runtime/6819213/TestBootNativeLibraryPath.java
Normal file
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2008 Sun Microsystems, Inc. All Rights Reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code 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 General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
|
||||
* CA 95054 USA or visit www.sun.com if you need additional information or
|
||||
* have any questions.
|
||||
*/
|
||||
|
||||
/*
|
||||
* @test TestBootNativeLibraryPath.java
|
||||
* @bug 6819213
|
||||
* @compile -XDignore.symbol.file TestBootNativeLibraryPath.java
|
||||
* @summary verify sun.boot.native.library.path is expandable on 32 bit systems
|
||||
* @run main TestBootNativeLibraryPath
|
||||
* @author ksrini
|
||||
*/
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.ToolProvider;
|
||||
|
||||
public class TestBootNativeLibraryPath {
|
||||
|
||||
private static final String TESTFILE = "Test6";
|
||||
|
||||
static void createTestClass() throws IOException {
|
||||
FileOutputStream fos = new FileOutputStream(TESTFILE + ".java");
|
||||
PrintStream ps = new PrintStream(fos);
|
||||
ps.println("public class " + TESTFILE + "{");
|
||||
ps.println("public static void main(String[] args) {\n");
|
||||
ps.println("System.out.println(System.getProperty(\"sun.boot.library.path\"));\n");
|
||||
ps.println("}}\n");
|
||||
ps.close();
|
||||
fos.close();
|
||||
|
||||
JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
|
||||
String javacOpts[] = {TESTFILE + ".java"};
|
||||
if (javac.run(null, null, null, javacOpts) != 0) {
|
||||
throw new RuntimeException("compilation of " + TESTFILE + ".java Failed");
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> doExec(String... args) {
|
||||
String javaCmd = System.getProperty("java.home") + "/bin/java";
|
||||
if (!new File(javaCmd).exists()) {
|
||||
javaCmd = System.getProperty("java.home") + "/bin/java.exe";
|
||||
}
|
||||
|
||||
ArrayList<String> cmds = new ArrayList<String>();
|
||||
cmds.add(javaCmd);
|
||||
for (String x : args) {
|
||||
cmds.add(x);
|
||||
}
|
||||
System.out.println("cmds=" + cmds);
|
||||
ProcessBuilder pb = new ProcessBuilder(cmds);
|
||||
|
||||
Map<String, String> env = pb.environment();
|
||||
pb.directory(new File("."));
|
||||
|
||||
List<String> out = new ArrayList<String>();
|
||||
try {
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
BufferedReader rd = new BufferedReader(new InputStreamReader(p.getInputStream()),8192);
|
||||
String in = rd.readLine();
|
||||
while (in != null) {
|
||||
out.add(in);
|
||||
System.out.println(in);
|
||||
in = rd.readLine();
|
||||
}
|
||||
int retval = p.waitFor();
|
||||
p.destroy();
|
||||
if (retval != 0) {
|
||||
throw new RuntimeException("Error: test returned non-zero value");
|
||||
}
|
||||
return out;
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
throw new RuntimeException(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
if (!System.getProperty("sun.arch.data.model").equals("32")) {
|
||||
System.out.println("Warning: test skipped for 64-bit systems\n");
|
||||
return;
|
||||
}
|
||||
String osname = System.getProperty("os.name");
|
||||
if (osname.startsWith("Windows")) {
|
||||
osname = "Windows";
|
||||
}
|
||||
|
||||
createTestClass();
|
||||
|
||||
// Test a simple path
|
||||
String libpath = File.pathSeparator + "tmp" + File.pathSeparator + "foobar";
|
||||
List<String> processOut = null;
|
||||
String sunbootlibrarypath = "-Dsun.boot.library.path=" + libpath;
|
||||
processOut = doExec(sunbootlibrarypath, "-cp", ".", TESTFILE);
|
||||
if (processOut == null || !processOut.get(0).endsWith(libpath)) {
|
||||
throw new RuntimeException("Error: did not get expected error string");
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException("Unexpected error " + ex);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user