Merge branch 'release/5.0.0b3'

This commit is contained in:
Gravit 2019-04-29 16:16:40 +07:00
commit 5e0bcf5154
No known key found for this signature in database
GPG key ID: 061981E1E85D3216
36 changed files with 367 additions and 348 deletions

View file

@ -1,5 +1,6 @@
package ru.gravit.launchserver;
import io.netty.handler.logging.LogLevel;
import ru.gravit.launcher.Launcher;
import ru.gravit.launcher.LauncherConfig;
import ru.gravit.launcher.NeedGarbageCollection;
@ -80,6 +81,8 @@ public static final class Config {
public String binaryName;
public boolean copyBinaries = true;
public LauncherConfig.LauncherEnvironment env;
// Handlers & Providers
@ -269,7 +272,7 @@ public class LauncherConf
}
public class NettyConfig {
public boolean clientEnabled;
public boolean fileServerEnabled;
public boolean sendExceptionEnabled;
public String launcherURL;
public String downloadURL;
@ -278,6 +281,7 @@ public class NettyConfig {
public Map<String, String> bindings = new HashMap<>();
public NettyPerformanceConfig performance;
public NettyBindAddress[] binds;
public LogLevel logLevel = LogLevel.DEBUG;
}
public class NettyPerformanceConfig
{
@ -678,6 +682,7 @@ private void generateConfigIfNotExists(boolean testEnv) throws IOException {
Config newConfig = new Config();
newConfig.mirrors = new String[]{"http://mirror.gravitlauncher.ml/", "https://mirror.gravit.pro/"};
newConfig.launch4j = new ExeConf();
newConfig.launch4j.enabled = true;
newConfig.launch4j.copyright = "© GravitLauncher Team";
newConfig.launch4j.fileDesc = "GravitLauncher ".concat(Launcher.getVersion().getVersionString());
newConfig.launch4j.fileVer = Launcher.getVersion().getVersionString().concat(".").concat(String.valueOf(Launcher.getVersion().patch));
@ -704,11 +709,7 @@ private void generateConfigIfNotExists(boolean testEnv) throws IOException {
newConfig.whitelistRejectString = "Вас нет в белом списке";
newConfig.netty = new NettyConfig();
newConfig.netty.address = "ws://localhost:9274/api";
newConfig.netty.downloadURL = "http://localhost:9274/%dirname%/";
newConfig.netty.launcherURL = "http://localhost:9274/Launcher.jar";
newConfig.netty.launcherEXEURL = "http://localhost:9274/Launcher.exe";
newConfig.netty.clientEnabled = false;
newConfig.netty.fileServerEnabled = true;
newConfig.netty.binds = new NettyBindAddress[]{ new NettyBindAddress("0.0.0.0", 9274) };
newConfig.netty.performance = new NettyPerformanceConfig();
newConfig.netty.performance.bossThread = 2;
@ -720,7 +721,7 @@ private void generateConfigIfNotExists(boolean testEnv) throws IOException {
newConfig.threadCoreCount = 0; // on your own
newConfig.threadCount = JVMHelper.OPERATING_SYSTEM_MXBEAN.getAvailableProcessors() >= 4 ? JVMHelper.OPERATING_SYSTEM_MXBEAN.getAvailableProcessors() / 2 : JVMHelper.OPERATING_SYSTEM_MXBEAN.getAvailableProcessors();
newConfig.enabledRadon = true;
newConfig.enabledRadon = false;
newConfig.enabledProGuard = true;
newConfig.stripLineNumbers = true;
newConfig.deleteTempFiles = true;
@ -734,19 +735,20 @@ private void generateConfigIfNotExists(boolean testEnv) throws IOException {
newConfig.components.put("authLimiter", authLimiterComponent);
// Set server address
String address;
if (testEnv) {
newConfig.setLegacyAddress("localhost");
address = "localhost";
newConfig.setProjectName("test");
} else {
System.out.println("LaunchServer legacy address(default: localhost): ");
newConfig.setLegacyAddress(commandHandler.readLine());
System.out.println("LaunchServer address(default: localhost): ");
address = commandHandler.readLine();
System.out.println("LaunchServer projectName: ");
newConfig.setProjectName(commandHandler.readLine());
}
if(newConfig.legacyAddress == null)
if(address == null)
{
LogHelper.error("Legacy address null. Using localhost");
newConfig.legacyAddress = "localhost";
LogHelper.error("Address null. Using localhost");
address = "localhost";
}
if(newConfig.projectName == null)
{
@ -754,6 +756,13 @@ private void generateConfigIfNotExists(boolean testEnv) throws IOException {
newConfig.projectName = "MineCraft";
}
newConfig.legacyAddress = address;
newConfig.netty.address = "ws://" + address + ":9274/api";
newConfig.netty.downloadURL = "http://" + address + ":9274/%dirname%/";
newConfig.netty.launcherURL = "http://" + address + ":9274/Launcher.jar";
newConfig.netty.launcherEXEURL = "http://" + address + ":9274/Launcher.exe";
newConfig.netty.sendExceptionEnabled = true;
// Write LaunchServer config
LogHelper.info("Writing LaunchServer config file");
try (BufferedWriter writer = IOHelper.newWriter(configFile)) {
@ -836,14 +845,14 @@ public void syncUpdatesDir(Collection<String> dirs) throws IOException {
LogHelper.info("Syncing updates dir");
Map<String, SignedObjectHolder<HashedDir>> newUpdatesDirMap = new HashMap<>(16);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(updatesDir)) {
for (Path updateDir : dirStream) {
for (final Path updateDir : dirStream) {
if (Files.isHidden(updateDir))
continue; // Skip hidden
// Resolve name and verify is dir
String name = IOHelper.getFileName(updateDir);
if (!IOHelper.isDir(updateDir)) {
LogHelper.warning("Not update dir: '%s'", name);
if (!IOHelper.isFile(updateDir) && Arrays.asList(".jar", ".exe", ".hash").stream().noneMatch(e -> updateDir.toString().endsWith(e))) LogHelper.warning("Not update dir: '%s'", name);
continue;
}

View file

@ -35,7 +35,7 @@ public void clear() {
public EXEL4JLauncherBinary(LaunchServer server) {
super(server, server.dir.resolve(server.config.binaryName + ".exe"));
super(server, LauncherBinary.resolve(server, ".exe"));
faviconFile = server.dir.resolve("favicon.ico");
}

View file

@ -10,7 +10,7 @@
public class EXELauncherBinary extends LauncherBinary {
public EXELauncherBinary(LaunchServer server) {
super(server, server.dir.resolve(server.config.binaryName + ".exe"));
super(server, LauncherBinary.resolve(server, ".exe"));
}
@Override

View file

@ -24,9 +24,8 @@ public final class JARLauncherBinary extends LauncherBinary {
public List<Path> addonLibs;
public JARLauncherBinary(LaunchServer server) throws IOException {
super(server);
super(server, LauncherBinary.resolve(server, ".jar"));
count = new AtomicLong(0);
syncBinaryFile = server.dir.resolve(server.config.binaryName + ".jar");
runtimeDir = server.dir.resolve(Launcher.RUNTIME_DIR);
guardDir = server.dir.resolve(Launcher.GUARD_DIR);
buildDir = server.dir.resolve("build");

View file

@ -10,7 +10,7 @@
public abstract class LauncherBinary {
public final LaunchServer server;
public Path syncBinaryFile;
public final Path syncBinaryFile;
private volatile DigestBytesHolder binary;
private volatile byte[] sign;
@ -19,10 +19,6 @@ protected LauncherBinary(LaunchServer server, Path binaryFile) {
syncBinaryFile = binaryFile;
}
protected LauncherBinary(LaunchServer server) {
this.server = server;
}
public abstract void build() throws IOException;
@ -49,4 +45,8 @@ public final boolean sync() throws IOException {
return exists;
}
public static final Path resolve(LaunchServer server, String ext) {
return server.config.copyBinaries ? server.updatesDir.resolve(server.config.binaryName + ext) : server.dir.resolve(server.config.binaryName + ext);
}
}

View file

@ -32,7 +32,7 @@ public LauncherNettyServer() {
serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
.handler(new LoggingHandler(config.logLevel))
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
public void initChannel(NioSocketChannel ch) {
@ -42,7 +42,7 @@ public void initChannel(NioSocketChannel ch) {
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new WebSocketServerCompressionHandler());
pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true));
pipeline.addLast(new FileServerHandler(LaunchServer.server.updatesDir, true));
if (LaunchServer.server.config.netty.fileServerEnabled) pipeline.addLast(new FileServerHandler(LaunchServer.server.updatesDir, true));
pipeline.addLast(new WebSocketFrameHandler());
}
});

View file

@ -60,7 +60,7 @@ public void execute(ChannelHandlerContext ctx, Client clientData) throws Excepti
try {
AuthRequestEvent result = new AuthRequestEvent();
String ip = IOHelper.getIP(ctx.channel().remoteAddress());
if ((authType == null || authType == ConnectTypes.CLIENT) && !clientData.checkSign) {
if ((authType == null || authType == ConnectTypes.CLIENT) && ( clientData == null || !clientData.checkSign )) {
AuthProvider.authError("Don't skip Launcher Update");
return;
}

View file

@ -20,9 +20,9 @@
-keepattributes Signature
-adaptresourcefilecontents META-INF/MANIFEST.MF
-keeppackagenames com.mojang.**,net.minecraftforge.fml.**,cpw.mods.fml.**,oshi.**,com.sun.jna.**,com.google.gson.**,org.slf4j.**,oshi.jna.**,com.sun.jna.**,org.apache.commons.logging.**, org.fusesource.**, com.jfoenix.**
-keeppackagenames com.mojang.**,net.minecraftforge.fml.**,cpw.mods.fml.**,com.google.gson.**,ru.gravit.repackage.**,org.fusesource.**
-keep class com.mojang.**,net.minecraftforge.fml.**,cpw.mods.fml.**,oshi.**,com.sun.jna.**,com.google.gson.**,org.slf4j.**,oshi.jna.**,com.sun.jna.**,org.apache.commons.logging.**, org.fusesource.**, com.jfoenix.** {
-keep class com.mojang.**,net.minecraftforge.fml.**,cpw.mods.fml.**,com.google.gson.**,ru.gravit.repackage.**,org.fusesource.** {
*;
}

View file

@ -30,16 +30,18 @@
shadowJar {
classifier = null
relocate 'org.objectweb.asm', 'ru.gravit.repackage.org.objectweb.asm'
relocate 'io.netty', 'ru.gravit.repackage.io.netty'
configurations = [project.configurations.pack]
exclude 'module-info.class'
}
dependencies {
pack project(':LauncherAPI') // Not error on obf.
pack project(':LauncherAPI')
bundle 'com.github.oshi:oshi-core:3.13.0'
bundle 'com.jfoenix:jfoenix:8.0.8'
bundle 'de.jensd:fontawesomefx:8.9'
bundle 'org.fusesource.jansi:jansi:1.17.1'
bundle 'org.apache.httpcomponents:httpclient:4.5.7'
pack 'io.netty:netty-all:4.1.32.Final'
pack 'org.ow2.asm:asm-tree:7.1'
}

View file

@ -59,6 +59,8 @@ function initLoginScene() {
savePasswordBox = pane.lookup("#rememberchb");
savePasswordBox.setSelected(settings.login === null || settings.rsaPassword !== null);
authOptions = pane.lookup("#authOptions");
var link = pane.lookup("#link");
link.setText(config.linkText);
link.setOnAction(function(event) app.getHostServices().showDocument(config.linkURL.toURI()));

View file

@ -42,7 +42,7 @@ var settingsOverlay = {
settingsOverlay.updateRAMLabel();
var ramSlider = holder.lookup("#ramSlider");
ramSlider.setMax(JVMHelper.RAM);
ramSlider.setMax(FunctionalBridge.getJVMTotalMemory());
ramSlider.setSnapToTicks(true);
ramSlider.setShowTickMarks(true);
ramSlider.setShowTickLabels(true);

View file

@ -7,7 +7,8 @@
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.MethodNode;
import ru.gravit.utils.NativeJVMHalt;
import ru.gravit.launcher.utils.NativeJVMHalt;
import ru.gravit.utils.helper.LogHelper;
import java.io.ByteArrayOutputStream;

View file

@ -9,7 +9,8 @@
import ru.gravit.launcher.managers.ClientGsonManager;
import ru.gravit.launcher.managers.ConsoleManager;
import ru.gravit.launcher.request.Request;
import ru.gravit.launcher.request.websockets.ClientWebSocketService;
import ru.gravit.launcher.request.RequestException;
import ru.gravit.launcher.request.auth.RestoreSessionRequest;
import ru.gravit.launcher.request.websockets.StandartClientWebSocketService;
import ru.gravit.utils.helper.CommonHelper;
import ru.gravit.utils.helper.EnvHelper;
@ -72,11 +73,28 @@ public void start(String... args) throws Throwable {
if (runtimeProvider == null) runtimeProvider = new JSRuntimeProvider();
runtimeProvider.init(false);
runtimeProvider.preLoad();
if(Request.service != null)
if(Request.service == null)
{
String address = Launcher.getConfig().address;
LogHelper.debug("Start async connection to %s", address);
Request.service = StandartClientWebSocketService.initWebSockets(address, true);
Request.service.reconnectCallback = () ->
{
LogHelper.debug("WebSocket connect closed. Try reconnect");
try {
Request.service.open();
LogHelper.debug("Connect to %s", Launcher.getConfig().address);
} catch (Exception e) {
LogHelper.error(e);
throw new RequestException(String.format("Connect error: %s", e.getMessage() != null ? e.getMessage() : "null"));
}
try {
RestoreSessionRequest request1 = new RestoreSessionRequest(Request.getSession());
request1.request();
} catch (Exception e) {
LogHelper.error(e);
}
};
}
LauncherGuardManager.initGuard(false);
Objects.requireNonNull(args, "args");

View file

@ -3,17 +3,18 @@
import ru.gravit.launcher.*;
import ru.gravit.launcher.guard.LauncherGuardManager;
import ru.gravit.launcher.gui.JSRuntimeProvider;
import ru.gravit.launcher.hasher.DirWatcher;
import ru.gravit.launcher.hasher.FileNameMatcher;
import ru.gravit.launcher.hasher.HashedDir;
import ru.gravit.launcher.managers.ClientGsonManager;
import ru.gravit.launcher.profiles.ClientProfile;
import ru.gravit.launcher.profiles.PlayerProfile;
import ru.gravit.launcher.request.Request;
import ru.gravit.launcher.request.RequestException;
import ru.gravit.launcher.request.auth.RestoreSessionRequest;
import ru.gravit.launcher.serialize.HInput;
import ru.gravit.launcher.serialize.HOutput;
import ru.gravit.launcher.serialize.stream.StreamObject;
import ru.gravit.launcher.utils.DirWatcher;
import ru.gravit.utils.PublicURLClassLoader;
import ru.gravit.utils.helper.*;
import ru.gravit.utils.helper.JVMHelper.OS;
@ -36,7 +37,6 @@
import java.util.*;
public final class ClientLauncher {
private static final class ClassPathFileVisitor extends SimpleFileVisitor<Path> {
private final Collection<Path> result;
@ -328,7 +328,7 @@ public static Process launch(
context.playerProfile = params.pp;
context.args.add(javaBin.toString());
context.args.add(MAGICAL_INTEL_OPTION);
if (params.ram > 0 && params.ram <= JVMHelper.RAM) {
if (params.ram > 0 && params.ram <= FunctionalBridge.getJVMTotalMemory()) {
context.args.add("-Xms" + params.ram + 'M');
context.args.add("-Xmx" + params.ram + 'M');
}
@ -460,10 +460,11 @@ public static void main(String... args) throws Throwable {
{
LogHelper.debug("WebSocket connect closed. Try reconnect");
try {
if (!Request.service.reconnectBlocking()) LogHelper.error("Error connecting");
Request.service.open();
LogHelper.debug("Connect to %s", Launcher.getConfig().address);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
LogHelper.error(e);
throw new RequestException(String.format("Connect error: %s", e.getMessage() != null ? e.getMessage() : "null"));
}
try {
RestoreSessionRequest request1 = new RestoreSessionRequest(Request.getSession());
@ -537,8 +538,21 @@ public static void verifyHDir(Path dir, HashedDir hdir, FileNameMatcher matcher,
// Hash directory and compare (ignore update-only matcher entries, it will break offline-mode)
HashedDir currentHDir = new HashedDir(dir, matcher, true, digest);
if (!hdir.diff(currentHDir, matcher).isSame())
HashedDir.Diff diff = hdir.diff(currentHDir, matcher);
if (!diff.isSame())
{
/*AtomicBoolean isFoundFile = new AtomicBoolean(false);
diff.extra.walk(File.separator, (e,k,v) -> {
if(v.getType().equals(HashedEntry.Type.FILE)) { LogHelper.error("Extra file %s", e); isFoundFile.set(true); }
else LogHelper.error("Extra %s", e);
});
diff.mismatch.walk(File.separator, (e,k,v) -> {
if(v.getType().equals(HashedEntry.Type.FILE)) { LogHelper.error("Mismatch file %s", e); isFoundFile.set(true); }
else LogHelper.error("Mismatch %s", e);
});
if(isFoundFile.get())*/
throw new SecurityException(String.format("Forbidden modification: '%s'", IOHelper.getFileName(dir)));
}
}
private ClientLauncher() {

View file

@ -11,7 +11,6 @@
import ru.gravit.launcher.managers.HasherManager;
import ru.gravit.launcher.managers.HasherStore;
import ru.gravit.launcher.request.Request;
import ru.gravit.launcher.request.websockets.RequestInterface;
import ru.gravit.launcher.serialize.signed.SignedObjectHolder;
import ru.gravit.utils.helper.LogHelper;
@ -31,6 +30,8 @@ public class FunctionalBridge {
@LauncherAPI
public static Thread getHWID = null;
private static long cachedMemorySize = -1;
@LauncherAPI
public static HashedDirRunnable offlineUpdateRequest(String dirName, Path dir, SignedObjectHolder<HashedDir> hdir, FileNameMatcher matcher, boolean digest) {
return () -> {
@ -42,11 +43,6 @@ public static HashedDirRunnable offlineUpdateRequest(String dirName, Path dir, S
};
}
@LauncherAPI
public static void makeJsonRequest(RequestInterface request, Runnable callback) {
}
@LauncherAPI
public static void startTask(Runnable task) {
threadPool.execute(task);
@ -60,8 +56,9 @@ public static HWID getHWID() {
}
@LauncherAPI
public static long getTotalMemory() {
return hwidProvider.getTotalMemory() >> 20;
public static int getTotalMemory() {
if (cachedMemorySize > 0) return (int)cachedMemorySize;
return (int)(cachedMemorySize = hwidProvider.getTotalMemory() >> 20);
}
@LauncherAPI
@ -70,7 +67,7 @@ public static int getClientJVMBits() {
}
@LauncherAPI
public static long getJVMTotalMemory() {
public static int getJVMTotalMemory() {
if (getClientJVMBits() == 32) {
return Math.min(getTotalMemory(), 1536);
} else {
@ -106,6 +103,7 @@ public static void setAuthParams(AuthRequestEvent event) {
public interface HashedDirRunnable {
SignedObjectHolder<HashedDir> run() throws Exception;
}
@LauncherAPI
public static void evalCommand(String cmd)
{

View file

@ -83,7 +83,7 @@ public String getMacAddr() {
}
public long getTotalMemory() {
if (noHWID) return -1;
if (noHWID) return 1024<<20;
if (hardware == null) hardware = systemInfo.getHardware();
return hardware.getMemory().getTotal();
}

View file

@ -1,8 +1,11 @@
package ru.gravit.launcher.hasher;
package ru.gravit.launcher.utils;
import ru.gravit.launcher.LauncherAPI;
import ru.gravit.launcher.hasher.FileNameMatcher;
import ru.gravit.launcher.hasher.HashedDir;
import ru.gravit.launcher.hasher.HashedEntry;
import ru.gravit.launcher.hasher.HashedFile;
import ru.gravit.launcher.hasher.HashedEntry.Type;
import ru.gravit.utils.NativeJVMHalt;
import ru.gravit.utils.helper.IOHelper;
import ru.gravit.utils.helper.JVMHelper;
import ru.gravit.utils.helper.JVMHelper.OS;

View file

@ -1,4 +1,4 @@
package ru.gravit.utils;
package ru.gravit.launcher.utils;
import cpw.mods.fml.SafeExitJVMLegacy;
import net.minecraftforge.fml.SafeExitJVM;

View file

@ -3,8 +3,8 @@
dependencies {
compile project(':libLauncher')
compile 'org.java-websocket:Java-WebSocket:1.3.9'
compile 'org.apache.httpcomponents:httpclient:4.5.7'
compileOnly 'io.netty:netty-all:4.1.32.Final'
compileOnly 'org.apache.httpcomponents:httpclient:4.5.7'
compileOnly 'com.google.guava:guava:26.0-jre'
compile files('../compat/authlib/authlib-clean.jar')
}

View file

@ -174,11 +174,19 @@ public UpdateRequestEvent requestDo() throws Exception {
HashedDir.Diff diff = e.hdir.diff(localDir, matcher);
final List<ListDownloader.DownloadTask> adds = new ArrayList<>();
diff.mismatch.walk(IOHelper.CROSS_SEPARATOR, (path, name, entry) -> {
if(entry.getType() == HashedEntry.Type.FILE) {
if(entry.getType().equals(HashedEntry.Type.FILE)) {
HashedFile file = (HashedFile) entry;
totalSize += file.size;
adds.add(new ListDownloader.DownloadTask(path, file.size));
}
else if(entry.getType().equals(HashedEntry.Type.DIR))
{
try {
Files.createDirectories(dir.resolve(path));
} catch (IOException ex) {
LogHelper.error(ex);
}
}
});
totalSize = diff.mismatch.size();
startTime = Instant.now();

View file

@ -1,36 +1,101 @@
package ru.gravit.launcher.request.websockets;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import ru.gravit.utils.helper.LogHelper;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
public class ClientJSONPoint extends WebSocketClient {
public abstract class ClientJSONPoint {
public ClientJSONPoint(URI serverUri, Map<String, String> httpHeaders, int connectTimeout) {
super(serverUri, new Draft_6455(), httpHeaders, connectTimeout);
private final URI uri;
protected Channel ch;
private static final EventLoopGroup group = new NioEventLoopGroup();
protected WebSocketClientHandler webSocketClientHandler;
protected Bootstrap bootstrap = new Bootstrap();
public boolean isClosed;
public ClientJSONPoint(final String uri) throws SSLException {
this(URI.create(uri));
}
@Override
public void onOpen(ServerHandshake handshakedata) {
public ClientJSONPoint(URI uri) throws SSLException {
this.uri = uri;
String protocol = uri.getScheme();
if (!"ws".equals(protocol) && !"wss".equals(protocol)) {
throw new IllegalArgumentException("Unsupported protocol: " + protocol);
}
boolean ssl = false;
if("wss".equals(protocol))
{
ssl = true;
}
final SslContext sslCtx;
if (ssl) {
sslCtx = SslContextBuilder.forClient().build();
} else sslCtx = null;
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
pipeline.addLast("http-codec", new HttpClientCodec());
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
pipeline.addLast("ws-handler", webSocketClientHandler);
}
});
}
@Override
public void onMessage(String message) {
public void open() throws Exception {
//System.out.println("WebSocket Client connecting");
webSocketClientHandler =
new WebSocketClientHandler(
WebSocketClientHandshakerFactory.newHandshaker(
uri, WebSocketVersion.V13, null, false, EmptyHttpHeaders.INSTANCE, 1280000), this);
ch = bootstrap.connect(uri.getHost(), uri.getPort()).sync().channel();
webSocketClientHandler.handshakeFuture().sync();
}
public ChannelFuture send(String text)
{
LogHelper.dev("Send: %s", text);
return ch.writeAndFlush(new TextWebSocketFrame(text));
}
abstract void onMessage(String message) throws Exception;
abstract void onDisconnect() throws Exception;
abstract void onOpen() throws Exception;
public void close() throws InterruptedException {
//System.out.println("WebSocket Client sending close");
isClosed = true;
if(ch != null && ch.isActive())
{
ch.writeAndFlush(new CloseWebSocketFrame());
ch.closeFuture().sync();
}
//group.shutdownGracefully();
}
@Override
public void onClose(int code, String reason, boolean remote) {
LogHelper.debug("Disconnected: " + code + " " + remote + " " + reason != null ? reason : "no reason");
}
@Override
public void onError(Exception ex) {
LogHelper.error(ex);
public void eval(final String text) throws IOException {
ch.writeAndFlush(new TextWebSocketFrame(text));
}
}

View file

@ -2,7 +2,6 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.java_websocket.handshake.ServerHandshake;
import ru.gravit.launcher.events.ExceptionEvent;
import ru.gravit.launcher.events.request.*;
import ru.gravit.launcher.hasher.HashedEntry;
@ -10,10 +9,10 @@
import ru.gravit.launcher.request.ResultInterface;
import ru.gravit.utils.helper.LogHelper;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -27,8 +26,8 @@ public class ClientWebSocketService extends ClientJSONPoint {
private HashMap<String, Class<? extends ResultInterface>> results;
private HashSet<EventHandler> handlers;
public ClientWebSocketService(GsonBuilder gsonBuilder, String address, int i) {
super(createURL(address), Collections.emptyMap(), i);
public ClientWebSocketService(GsonBuilder gsonBuilder, String address, int i) throws SSLException {
super(createURL(address));
requests = new HashMap<>();
results = new HashMap<>();
handlers = new HashSet<>();
@ -51,7 +50,7 @@ private static URI createURL(String address) {
}
@Override
public void onMessage(String message) {
void onMessage(String message) {
ResultInterface result = gson.fromJson(message, ResultInterface.class);
for (EventHandler handler : handlers) {
handler.process(result);
@ -59,25 +58,19 @@ public void onMessage(String message) {
}
@Override
public void onError(Exception e) {
LogHelper.error(e);
void onDisconnect() {
LogHelper.info("WebSocket client disconnect");
if(onCloseCallback != null) onCloseCallback.onClose(0,"unsupported param", !isClosed);
}
@Override
public void onOpen(ServerHandshake handshakedata) {
//Notify open
void onOpen() throws Exception {
synchronized (onConnect)
{
onConnect.notifyAll();
}
}
@Override
public void onClose(int code, String reason, boolean remote)
{
LogHelper.debug("Disconnected: " + code + " " + remote + " " + (reason != null ? reason : "no reason"));
if(onCloseCallback != null)
onCloseCallback.onClose(code, reason, remote);
}
@FunctionalInterface
public interface OnCloseCallback
{
@ -136,7 +129,7 @@ public void registerHandler(EventHandler eventHandler) {
}
public void waitIfNotConnected()
{
if(!isOpen() && !isClosed() && !isClosing())
/*if(!isOpen() && !isClosed() && !isClosing())
{
LogHelper.warning("WebSocket not connected. Try wait onConnect object");
synchronized (onConnect)
@ -147,20 +140,22 @@ public void waitIfNotConnected()
LogHelper.error(e);
}
}
}
}*/
}
public void sendObject(Object obj) throws IOException {
waitIfNotConnected();
if(isClosed() && reconnectCallback != null)
reconnectCallback.onReconnect();
if(ch == null || !ch.isActive()) reconnectCallback.onReconnect();
//if(isClosed() && reconnectCallback != null)
// reconnectCallback.onReconnect();
send(gson.toJson(obj, RequestInterface.class));
}
public void sendObject(Object obj, Type type) throws IOException {
waitIfNotConnected();
if(isClosed() && reconnectCallback != null)
reconnectCallback.onReconnect();
if(ch == null || !ch.isActive()) reconnectCallback.onReconnect();
//if(isClosed() && reconnectCallback != null)
// reconnectCallback.onReconnect();
send(gson.toJson(obj, type));
}

View file

@ -8,6 +8,7 @@
import ru.gravit.utils.helper.JVMHelper;
import ru.gravit.utils.helper.LogHelper;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@ -15,7 +16,7 @@
public class StandartClientWebSocketService extends ClientWebSocketService {
public WaitEventHandler waitEventHandler = new WaitEventHandler();
public StandartClientWebSocketService(GsonBuilder gsonBuilder, String address, int i) {
public StandartClientWebSocketService(GsonBuilder gsonBuilder, String address, int i) throws SSLException {
super(gsonBuilder, address, i);
}
public class RequestFuture implements Future<ResultInterface>
@ -101,27 +102,38 @@ public RequestFuture asyncSendRequest(RequestInterface request) throws IOExcepti
}
public static StandartClientWebSocketService initWebSockets(String address, boolean async) {
StandartClientWebSocketService service = new StandartClientWebSocketService(new GsonBuilder(), address, 5000);
StandartClientWebSocketService service;
try {
service = new StandartClientWebSocketService(new GsonBuilder(), address, 5000);
} catch (SSLException e) {
LogHelper.error(e);
return null;
}
service.registerResults();
service.registerRequests();
service.registerHandler(service.waitEventHandler);
if(!async)
{
try {
if (!service.connectBlocking()) LogHelper.error("Error connecting");
service.open();
LogHelper.debug("Connect to %s", address);
} catch (InterruptedException e) {
} catch (Exception e) {
e.printStackTrace();
}
}
else
{
service.connect();
try {
service.open();
} catch (Exception e) {
e.printStackTrace();
}
}
JVMHelper.RUNTIME.addShutdownHook(new Thread(() -> {
try {
if(service.isOpen())
service.closeBlocking();
//if(service.isOpen())
// service.closeBlocking();
service.close();
} catch (InterruptedException e) {
LogHelper.error(e);
}

View file

@ -0,0 +1,92 @@
package ru.gravit.launcher.request.websockets;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.util.CharsetUtil;
import ru.gravit.utils.helper.LogHelper;
public class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {
private final WebSocketClientHandshaker handshaker;
private final ClientJSONPoint clientJSONPoint;
private ChannelPromise handshakeFuture;
public WebSocketClientHandler(final WebSocketClientHandshaker handshaker, ClientJSONPoint clientJSONPoint) {
this.handshaker = handshaker;
this.clientJSONPoint = clientJSONPoint;
}
public ChannelFuture handshakeFuture() {
return handshakeFuture;
}
@Override
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
handshakeFuture = ctx.newPromise();
}
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
handshaker.handshake(ctx.channel());
clientJSONPoint.onOpen();
}
@Override
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
//System.out.println("WebSocket Client disconnected!");
clientJSONPoint.onDisconnect();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
final Channel ch = ctx.channel();
if (!handshaker.isHandshakeComplete()) {
// web socket client connected
handshaker.finishHandshake(ch, (FullHttpResponse) msg);
handshakeFuture.setSuccess();
return;
}
if (msg instanceof FullHttpResponse) {
final FullHttpResponse response = (FullHttpResponse) msg;
throw new Exception("Unexpected FullHttpResponse (getStatus=" + response.status() + ", content="
+ response.content().toString(CharsetUtil.UTF_8) + ')');
}
final WebSocketFrame frame = (WebSocketFrame) msg;
if (frame instanceof TextWebSocketFrame) {
final TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
clientJSONPoint.onMessage(textFrame.text());
LogHelper.dev("Message: %s", textFrame.text());
// uncomment to print request
// logger.info(textFrame.text());
} else if (frame instanceof PongWebSocketFrame) {
} else if (frame instanceof CloseWebSocketFrame)
ch.close();
else if (frame instanceof BinaryWebSocketFrame) {
// uncomment to print request
// logger.info(frame.content().toString());
}
}
@Override
public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception {
LogHelper.error(cause);
if (!handshakeFuture.isDone()) {
handshakeFuture.setFailure(cause);
}
ctx.close();
}
}

View file

@ -2,7 +2,8 @@
* Discord channel: https://discord.gg/aJK6nMN
* [See license](LICENSE)
* [See code of conduct](CODE_OF_CONDUCT.md)
* [WIKI](https://launcher.gravit.pro)
* [WIKI](https://yii2.gravit.pro)
* [OLD WIKI (4.X.X)](https://launcher.gravit.pro)
* Get it (requires cURL):
```sh
curl -s https://raw.githubusercontent.com/GravitLauncher/Launcher/master/get_it.sh | sh

View file

@ -1,6 +1,12 @@
apply plugin: 'com.github.johnrengelman.shadow'
String mainClassName = "ru.gravit.launcher.server.ServerWrapper"
String mainAgentName = "ru.gravit.launcher.server.ServerAgent"
configurations {
pack
compile.extendsFrom pack
}
repositories {
maven {
@ -12,6 +18,7 @@
targetCompatibility = '1.8'
jar {
classifier = 'clean'
from { configurations.runtime.collect { it.isDirectory() ? it : zipTree(it) } }
manifest.attributes("Main-Class": mainClassName,
"Premain-Class": mainAgentName,
@ -21,5 +28,18 @@
}
dependencies {
compile project(':LauncherAPI')
pack project(':LauncherAPI')
pack 'org.apache.httpcomponents:httpclient:4.5.7'
pack 'io.netty:netty-all:4.1.32.Final'
}
shadowJar {
classifier = null
relocate 'io.netty', 'ru.gravit.repackage.io.netty'
configurations = [project.configurations.pack]
exclude 'module-info.class'
}
build.dependsOn tasks.shadowJar

View file

@ -6,6 +6,7 @@
import ru.gravit.launcher.events.request.ProfilesRequestEvent;
import ru.gravit.launcher.profiles.ClientProfile;
import ru.gravit.launcher.request.Request;
import ru.gravit.launcher.request.RequestException;
import ru.gravit.launcher.request.auth.AuthRequest;
import ru.gravit.launcher.request.update.ProfilesRequest;
import ru.gravit.launcher.server.setup.ServerWrapperSetup;
@ -162,15 +163,16 @@ public void run(String... args) throws Throwable {
{
LogHelper.debug("WebSocket connect closed. Try reconnect");
try {
if (!Request.service.reconnectBlocking()) LogHelper.error("Error connecting");
Request.service.open();
LogHelper.debug("Connect to %s", config.websocket.address);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
LogHelper.error(e);
throw new RequestException(String.format("Connect error: %s", e.getMessage() != null ? e.getMessage() : "null"));
}
auth();
};
}
LogHelper.info("ServerWrapper: Project %s, LaunchServer address: %s port %d. Title: %s", config.projectname, config.websocket.address, config.title);
LogHelper.info("ServerWrapper: Project %s, LaunchServer address: %s. Title: %s", config.projectname, config.websocket.address, config.title);
LogHelper.info("Minecraft Version (for profile): %s", wrapper.profile == null ? "unknown" : wrapper.profile.getVersion().name);
LogHelper.info("Start Minecraft Server");
LogHelper.debug("Invoke main method %s", mainClass.getName());

View file

@ -67,6 +67,7 @@ public void run() throws IOException {
wrapper.config.password = password;
wrapper.config.title = title;
wrapper.config.stopOnError = false;
wrapper.updateLauncherConfig();
if (wrapper.auth()) {
break;

View file

@ -60,7 +60,7 @@ public final class Launcher {
public static final int MAJOR = 5;
public static final int MINOR = 0;
public static final int PATCH = 0;
public static final int BUILD = 2;
public static final int BUILD = 3;
public static final Version.Type RELEASE = Version.Type.BETA;
public static GsonManager gsonManager;

View file

@ -27,7 +27,8 @@ public enum Version {
MC1122("1.12.2", 340),
MC113("1.13", 393),
MC1131("1.13.1", 401),
MC1132("1.13.2", 402);
MC1132("1.13.2", 402),
MC114("1.14", 477);
private static final Map<String, Version> VERSIONS;
static {

View file

@ -6,8 +6,6 @@
public class LauncherSSLContext {
public SSLServerSocketFactory ssf;
public SSLSocketFactory sf;
@SuppressWarnings("unused")
private SSLContext sc;
public LauncherSSLContext(KeyStore ks, String keypassword) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException {
TrustManager[] trustAllCerts = new TrustManager[]{

View file

@ -1,213 +0,0 @@
package ru.gravit.utils.downloader;
import ru.gravit.utils.helper.IOHelper;
import ru.gravit.utils.helper.LogHelper;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class Downloader implements Runnable {
@FunctionalInterface
public interface Handler {
void check(Certificate[] certs) throws IOException;
}
public static final Map<String, String> requestClient = Collections.singletonMap("User-Agent",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11");
public static final int INTERVAL = 300;
private final File file;
private final URL url;
private final String method;
public final Map<String, String> requestProps;
public AtomicInteger writed = new AtomicInteger(0);
public final AtomicBoolean interrupt = new AtomicBoolean(false);
public final AtomicBoolean interrupted = new AtomicBoolean(false);
public AtomicReference<Throwable> ex = new AtomicReference<>(null);
private final int skip;
private final Handler handler;
private HttpURLConnection connect = null;
public Downloader(URL url, File file) {
this.requestProps = new HashMap<>(requestClient);
this.file = file;
this.url = url;
this.skip = 0;
this.handler = null;
this.method = null;
}
public Downloader(URL url, File file, int skip) {
this.requestProps = new HashMap<>(requestClient);
this.file = file;
this.url = url;
this.skip = skip;
this.handler = null;
this.method = null;
}
public Downloader(URL url, File file, Handler handler) {
this.requestProps = new HashMap<>(requestClient);
this.file = file;
this.url = url;
this.skip = 0;
this.handler = handler;
this.method = null;
}
public Downloader(URL url, File file, int skip, Handler handler) {
this.requestProps = new HashMap<>(requestClient);
this.file = file;
this.url = url;
this.skip = skip;
this.handler = handler;
this.method = null;
}
public Downloader(URL url, File file, int skip, Handler handler, Map<String, String> requestProps) {
this.requestProps = new HashMap<>(requestProps);
this.file = file;
this.url = url;
this.skip = skip;
this.handler = handler;
this.method = null;
}
public Downloader(URL url, File file, int skip, Handler handler, Map<String, String> requestProps, String method) {
this.requestProps = new HashMap<>(requestProps);
this.file = file;
this.url = url;
this.skip = skip;
this.handler = handler;
this.method = method;
}
public Downloader(URL url, File file, int skip, Handler handler, String method) {
this.requestProps = new HashMap<>(requestClient);
this.file = file;
this.url = url;
this.skip = skip;
this.handler = handler;
this.method = method;
}
public Map<String, String> getProps() {
return requestProps;
}
public void addProp(String key, String value) {
requestProps.put(key, value);
}
public File getFile() {
return file;
}
public String getMethod() {
return method;
}
public Handler getHandler() {
return handler;
}
public void downloadFile() throws IOException {
if (!(url.getProtocol().equalsIgnoreCase("http") || url.getProtocol().equalsIgnoreCase("https")))
throw new IOException("Invalid protocol.");
interrupted.set(false);
if (url.getProtocol().equalsIgnoreCase("http")) {
HttpURLConnection connect = (HttpURLConnection) (url).openConnection();
this.connect = connect;
if (method != null) connect.setRequestMethod(method);
for (Map.Entry<String, String> ent : requestProps.entrySet()) {
connect.setRequestProperty(ent.getKey(), ent.getValue());
}
connect.setInstanceFollowRedirects(true);
if (!(connect.getResponseCode() >= 200 && connect.getResponseCode() < 300))
throw new IOException(String.format("Invalid response of http server %d.", connect.getResponseCode()));
try (BufferedInputStream in = new BufferedInputStream(connect.getInputStream(), IOHelper.BUFFER_SIZE);
FileOutputStream fout = new FileOutputStream(file, skip != 0)) {
byte data[] = new byte[IOHelper.BUFFER_SIZE];
int count = -1;
long timestamp = System.currentTimeMillis();
int writed_local = 0;
in.skip(skip);
while ((count = in.read(data)) != -1) {
fout.write(data, 0, count);
writed_local += count;
if (System.currentTimeMillis() - timestamp > INTERVAL) {
writed.set(writed_local);
LogHelper.debug("Downloaded %d", writed_local);
if (interrupt.get()) {
break;
}
}
}
LogHelper.debug("Downloaded %d", writed_local);
writed.set(writed_local);
}
} else {
HttpsURLConnection connect = (HttpsURLConnection) (url).openConnection();
this.connect = connect;
if (method != null) connect.setRequestMethod(method);
for (Map.Entry<String, String> ent : requestProps.entrySet()) {
connect.setRequestProperty(ent.getKey(), ent.getValue());
}
connect.setInstanceFollowRedirects(true);
if (handler != null)
handler.check(connect.getServerCertificates());
if (!(connect.getResponseCode() >= 200 && connect.getResponseCode() < 300))
throw new IOException(String.format("Invalid response of http server %d.", connect.getResponseCode()));
try (BufferedInputStream in = new BufferedInputStream(connect.getInputStream(), IOHelper.BUFFER_SIZE);
FileOutputStream fout = new FileOutputStream(file, skip != 0)) {
byte data[] = new byte[IOHelper.BUFFER_SIZE];
int count = -1;
long timestamp = System.currentTimeMillis();
int writed_local = 0;
in.skip(skip);
while ((count = in.read(data)) != -1) {
fout.write(data, 0, count);
writed_local += count;
if (System.currentTimeMillis() - timestamp > INTERVAL) {
writed.set(writed_local);
LogHelper.debug("Downloaded %d", writed_local);
if (interrupt.get()) {
break;
}
}
}
LogHelper.debug("Downloaded %d", writed_local);
writed.set(writed_local);
}
}
interrupted.set(true);
}
@Override
public void run() {
try {
downloadFile();
} catch (Throwable ex) {
this.ex.set(ex);
LogHelper.error(ex);
}
if (connect != null)
try {
connect.disconnect();
} catch (Throwable ignored) {
}
}
}

View file

@ -7,8 +7,6 @@
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.management.RuntimeMXBean;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
@ -52,8 +50,6 @@ public static OS byName(String name) {
public static final int OS_BITS = getCorrectOSArch();
@LauncherAPI
public static final int JVM_BITS = Integer.parseInt(System.getProperty("sun.arch.data.model"));
@LauncherAPI
public static final int RAM = getRAMAmount();
@LauncherAPI
public static final SecurityManager SECURITY_MANAGER = System.getSecurityManager();
@ -137,19 +133,6 @@ public static String getEnvPropertyCaseSensitive(String name) {
return System.getenv().get(name);
}
private static int getRAMAmount() {
int physicalRam = 1024;
try {
final Method getTotalPhysicalMemorySize = OPERATING_SYSTEM_MXBEAN.getClass().getDeclaredMethod("getTotalPhysicalMemorySize");
getTotalPhysicalMemorySize.setAccessible(true);
physicalRam = (int) ((long)getTotalPhysicalMemorySize.invoke(OPERATING_SYSTEM_MXBEAN) >> 20);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException
| SecurityException e) {
throw new Error(e);
}
return Math.min(physicalRam, OS_BITS == 32 ? 1536 : 32768); // Limit 32-bit OS to 1536 MiB, and 64-bit OS to 32768 MiB (because it's enough)
}
@LauncherAPI
public static boolean isJVMMatchesSystemArch() {
return JVM_BITS == OS_BITS;

View file

@ -15,6 +15,7 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;
public final class LogHelper {
@ -50,6 +51,7 @@ public enum OutputTypes {
}
private static final Set<OutputEnity> OUTPUTS = Collections.newSetFromMap(new ConcurrentHashMap<>(2));
private static final Set<Consumer<Throwable>> EXCEPTIONS_CALLBACKS = Collections.newSetFromMap(new ConcurrentHashMap<>(2));
private static final OutputEnity STD_OUTPUT;
private LogHelper() {
@ -60,6 +62,11 @@ public static void addOutput(OutputEnity output) {
OUTPUTS.add(Objects.requireNonNull(output, "output"));
}
@LauncherAPI
public static void addExcCallback(Consumer<Throwable> output) {
EXCEPTIONS_CALLBACKS.add(Objects.requireNonNull(output, "output"));
}
@LauncherAPI
public static void addOutput(Output output, OutputTypes type) {
OUTPUTS.add(new OutputEnity(Objects.requireNonNull(output, "output"), type));
@ -105,6 +112,7 @@ public static void dev(String format, Object... args) {
@LauncherAPI
public static void error(Throwable exc) {
EXCEPTIONS_CALLBACKS.forEach(e -> e.accept(exc));
error(isStacktraceEnabled() ? toString(exc) : exc.toString());
}