org.lanternpowered.pingy.Pingy.java Source code

Java tutorial

Introduction

Here is the source code for org.lanternpowered.pingy.Pingy.java

Source

/*
 * This file is part of Pingy, licensed under the MIT License (MIT).
 *
 * Copyright (c) LanternPowered <https://www.lanternpowered.org>
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the Software), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, andor sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.lanternpowered.pingy;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.timeout.ReadTimeoutHandler;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Pingy {

    private static DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
    private static boolean DEBUG_MODE = false;

    private static void log(PrintStream printStream, String msg) {
        printStream.printf("[%s] %s\n", TIME_FORMATTER.format(LocalDateTime.now()), msg);
    }

    public static void info(String msg) {
        log(System.out, msg);
    }

    public static void warn(String msg) {
        log(System.err, msg);
    }

    public static void debugInfo(String msg) {
        debug(() -> log(System.out, String.format("[DEBUG] %s", msg)));
    }

    public static void debugWarn(String msg) {
        debug(() -> log(System.err, String.format("[DEBUG] %s", msg)));
    }

    public static void debug(Runnable runnable) {
        if (DEBUG_MODE) {
            runnable.run();
        }
    }

    public static void main(String[] args) {
        final Path directory = Paths.get("");
        Path propsFile = new File("pingy.json").toPath();

        int index = 0;
        while (index < args.length) {
            final String arg = args[index++];
            switch (arg) {
            case "--config-path":
            case "--cp":
                if (index >= args.length) {
                    throw new IllegalArgumentException("The parameter \"--config-path\" doesn't have a value.");
                }
                final String value = args[index++];
                try {
                    propsFile = Paths.get(value);
                    // Try to parse the file to make sure it's valid
                    @SuppressWarnings("unused")
                    URL ignore = propsFile.toUri().toURL();
                    // May not be a directory
                    if (Files.isDirectory(propsFile)) {
                        throw new IllegalArgumentException(
                                "The config path is invalid: " + value + ", it may not be a directory.");
                    }
                } catch (MalformedURLException e) {
                    throw new IllegalArgumentException("The config path is invalid: " + value);
                }
                info("Set config path to: " + value);
                continue;
            case "--debug":
            case "--d":
                final String value0 = index < args.length ? args[index] : "--";
                if (value0.startsWith("--")) {
                    DEBUG_MODE = true;
                } else {
                    DEBUG_MODE = Boolean.parseBoolean(value0);
                    index++;
                }
                continue;
            // Any other properties?
            default:
                warn("Unknown launch parameter: " + arg);
            }
        }

        final PingyProperties properties;
        final boolean newlyCreated;
        if (Files.exists(propsFile)) {
            try (BufferedReader reader = Files.newBufferedReader(propsFile)) {
                properties = new Gson().fromJson(reader, PingyProperties.class);
            } catch (IOException e) {
                throw new IllegalStateException(
                        "Invalid properties file, try to resolve the issue or regenerate the file", e);
            }

            newlyCreated = false;
            info("Loading the properties file...");
        } else {
            properties = new PingyProperties();
            newlyCreated = true;
        }

        final Path parent = propsFile.getParent();
        if (parent != null && !Files.exists(parent)) {
            try {
                Files.createDirectories(parent);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        final Gson gson = new GsonBuilder().setPrettyPrinting().create();
        try (BufferedWriter writer = Files.newBufferedWriter(propsFile, StandardCharsets.UTF_8)) {
            gson.toJson(properties, writer);
            writer.flush();
        } catch (IOException e) {
            throw new IllegalStateException("Unable to write the properties file", e);
        }
        info(newlyCreated ? "Generating the properties file..." : "Updating the properties file...");

        try {
            properties.loadFavicon(directory);
        } catch (IOException e) {
            e.printStackTrace();
        }

        final Pingy pingy = new Pingy(properties);
        try {
            pingy.start();
            info("Pingy is successfully started.");
        } catch (IOException e) {
            throw new IllegalStateException("Unable to start the server", e);
        }
    }

    private final PingyProperties properties;

    public Pingy(PingyProperties properties) {
        this.properties = properties;
    }

    /**
     * Gets the {@link InetSocketAddress} that should be
     * used for the specified ip and port.
     *
     * @param ip The ip
     * @param port The port
     * @return The socket address
     */
    private static InetSocketAddress getBindAddress(String ip, int port) {
        if (ip.length() == 0) {
            return new InetSocketAddress(port);
        } else {
            return new InetSocketAddress(ip, port);
        }
    }

    /**
     * Starts the pingy server.
     *
     * @throws IOException
     */
    public void start() throws IOException {
        boolean epoll = false;

        if (this.properties.isUseEpollWhenAvailable()) {
            if (Epoll.isAvailable()) {
                debugInfo("Epoll is available");
                epoll = true;
            } else {
                debugWarn(
                        "Epoll is unavailable (The following exception is only used to print the cause why it's unavailable, "
                                + "it won't affect the functionality.)");
                //noinspection ThrowableResultOfMethodCallIgnored
                debug(() -> Epoll.unavailabilityCause().printStackTrace());
            }
        }

        final ServerBootstrap bootstrap = new ServerBootstrap();
        final EventLoopGroup group = epoll ? new EpollEventLoopGroup() : new NioEventLoopGroup();

        final ChannelFuture future = bootstrap.group(group)
                .channel(epoll ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new ReadTimeoutHandler(20))
                                .addLast(new PingyLegacyHandler(properties)).addLast(new PingyFramingHandler())
                                .addLast(new PingyHandler(properties));
                    }
                }).childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                .childOption(ChannelOption.TCP_NODELAY, true).childOption(ChannelOption.SO_KEEPALIVE, true)
                .bind(getBindAddress(this.properties.getIp(), this.properties.getPort()));
        final Channel channel = future.awaitUninterruptibly().channel();
        if (!channel.isActive()) {
            final Throwable cause = future.cause();
            if (cause instanceof BindException) {
                throw (BindException) cause;
            }
            throw new RuntimeException("Failed to bind to address", cause);
        }
        info("Successfully bound to: " + channel.localAddress());
    }
}