com.replaymod.replaystudio.launcher.Launcher.java Source code

Java tutorial

Introduction

Here is the source code for com.replaymod.replaystudio.launcher.Launcher.java

Source

/*
 * This file is part of ReplayStudio, licensed under the MIT License (MIT).
 *
 * Copyright (c) 2016 johni0702 <https://github.com/johni0702>
 * 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, and/or 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 com.replaymod.replaystudio.launcher;

import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Longs;
import com.google.gson.*;
import com.replaymod.replaystudio.Studio;
import com.replaymod.replaystudio.collection.ReplayPart;
import com.replaymod.replaystudio.filter.Filter;
import com.replaymod.replaystudio.replay.Replay;
import com.replaymod.replaystudio.studio.ReplayStudio;
import org.apache.commons.cli.*;
import org.apache.commons.lang3.StringUtils;

import java.io.*;
import java.util.*;
import java.util.regex.Pattern;

public class Launcher {

    public static void main(String[] args) throws Exception {
        //        Message msg = new TranslationMessage("key");
        //        System.out.println(msg.toJsonString());
        //        msg = Message.fromString(msg.toJsonString());
        //        System.out.println(msg.toJsonString());

        try {
            run(args);
        } catch (CriticalException e) {
            System.exit(e.getReturnCode());
        }
    }

    public static void run(String[] args) throws Exception {
        Options options = new Options();
        options.addOption("h", "help", false, "Shows the help page.");
        options.addOption("w", "wait", true, "[Debugging] Amount of seconds to wait before starting.");
        options.addOption("n", "no-wrapping", false, "[Debugging] Disables packet wrapping.");
        options.addOption("j", "json-config", true, "Use the supplied json config file to load instructions.");
        options.addOption("c", "config", true, "Use the supplied string to load instructions.");
        options.addOption("s", "stream", true,
                "Use streams instead of loading whole replays into RAM." + " (Only supported by stream filters)");
        options.addOption("p", "parts", true, "Splits the first replay at the specified position. "
                + "If supplied one timestamp either as milliseconds or as 10m37s420ms, splits at the target position. "
                + "If supplied multiple timestamps separated by \":\", splits at every position.\n"
                + "Every subsequent replay file will be used as the outputs file for each part of the split replay. "
                + "Use \"x\" as the outputs file to skip saving of that part.\n"
                + "This is equivalent to -c \"(<a,>b,>c,>d)(a|10m20s,1h42s|b,c,d)\"\n");
        options.addOption("a", "append", true,
                "Concatenates every supplied replay and saves the result in the last replay file.\n"
                        + "This is equivalent to -c \"(<a,<b,<c,>d)(a,b,c&d)\"");
        options.addOption("f", "filter", true,
                "Applies the specified filter to the supplied replay and saves the result in the second replay file.\n"
                        + "This is equivalent to -c \"(<a,>b)(a>SomeFilterName>b)\"");
        options.addOption("q", "squash", false,
                "Squash the supplied replay and save the result in the second replay file.\n"
                        + "This is equivalent to -c \"(<a,>b)(a[b)\"");
        options.addOption("r", "reverse", false, "Reverses the packet order in the specified replay and writes it"
                + "to the specified output file. Note that the output file if raw packet data, not a zipped replay with meta data.");
        options.addOption("d", "daemon", true,
                "Runs replay studio as a daemon listening for requests. Pass in the amount"
                        + "of concurrent worker threads. The port of the daemon is defined by the environment variable 'replaystudio.port' (default 4002).");

        CommandLineParser parser = new GnuParser();
        CommandLine cmd;
        try {
            cmd = parser.parse(options, args);
        } catch (ParseException e) {
            System.out.println(e.getMessage());
            throw new CriticalException(2);
        }

        if (cmd.hasOption('h')) {
            HelpFormatter formatter = new HelpFormatter();
            String footer = "Output files can be suffixed with \"!<name>!<singleplayer>!<time>\"\n"
                    + "where <singleplayer> is either \"true\" or \"false\", <name> is the server name or "
                    + "singleplayer world names and <time> the time in milliseconds at which the replay was recorded.\n"
                    + "Specifying only the name is also possible with \"!<name>\"."
                    + "Setting an output to \"x\" discards the replay.";
            formatter.printHelp(100, "<cmd> <options> <file> [file .. [file ..]]", "", options, footer);
            return;
        }

        if (cmd.hasOption('w')) {
            try {
                Thread.sleep((long) (Double.parseDouble(cmd.getOptionValue('w')) * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        if (cmd.hasOption('s') || cmd.hasOption('q')) {
            new StreamLauncher().launch(cmd);
        } else if (cmd.hasOption('r')) {
            new ReverseLauncher().launch(cmd);
        } else if (cmd.hasOption('d')) {
            new DaemonLauncher().launch(cmd);
        } else {
            new Launcher().launch(cmd);
        }
    }

    private final Map<String, String> inputs = new HashMap<>();
    private final Set<String> pipes = new HashSet<>();
    private final Map<String, String> outputs = new HashMap<>();
    private final List<Instruction> instructions = new ArrayList<>();

    public void launch(CommandLine cmd) throws FileNotFoundException {
        Studio studio = new ReplayStudio();
        if (cmd.hasOption('n')) {
            studio.setWrappingEnabled(false);
        }

        if (cmd.hasOption('j')) {
            JsonObject o;
            try {
                o = new JsonParser().parse(cmd.getOptionValue('j')).getAsJsonObject();
            } catch (JsonParseException | IllegalStateException e) {
                Reader reader;
                if ("".equals(cmd.getOptionValue('j'))) {
                    reader = new InputStreamReader(System.in);
                } else {
                    reader = new FileReader(cmd.getOptionValue('j'));
                }
                o = new JsonParser().parse(reader).getAsJsonObject();
            }
            parseConfig(studio, o);
        } else if (cmd.hasOption('c')) {
            parseConfig(studio, cmd.getOptionValue('c'), cmd.getArgs());
        } else if (cmd.hasOption('p')) {
            SplitInstruction instruction = new SplitInstruction();
            instruction.getInputs().add("a");
            inputs.put("a", cmd.getArgs()[0]);
            for (int i = 1; i < cmd.getArgs().length; i++) {
                outputs.put(String.valueOf(i), cmd.getArgs()[i]);
                instruction.getOutputs().add(String.valueOf(i));
            }
            instructions.add(instruction);
        } else if (cmd.hasOption('f')) {
            Filter filter = studio.loadFilter(cmd.getOptionValue('f'));
            FilterInstruction instruction = new FilterInstruction(studio, filter, new JsonObject());
            instruction.getInputs().add("a");
            instruction.getOutputs().add("b");
            inputs.put("a", cmd.getArgs()[0]);
            outputs.put("b", cmd.getArgs()[1]);
            instructions.add(instruction);
        } else {
            System.out.println("Missing instruction. Use --help to show help.");
            throw new CriticalException(2);
        }

        System.out.println(String.format("Generating %d replay(s) via %d pipe(s) from %d input(s)", outputs.size(),
                pipes.size(), inputs.size()));

        assertPossible();

        Map<String, ReplayPart> replays = new HashMap<>();

        // Read inputs
        int i = 1;
        for (Map.Entry<String, String> entry : inputs.entrySet()) {
            System.out.print(String.format("Reading input %d of %d...", i, inputs.size()));
            long time = System.nanoTime();
            try (FileInputStream in = new FileInputStream(new File(entry.getValue()))) {
                Replay replay = studio.createReplay(in, entry.getKey().charAt(0) == 'r');
                replays.put(entry.getKey(), replay);
            } catch (Throwable t) {
                System.out.println("Exception while reading input from " + entry.getValue());
                t.printStackTrace();
                throw new CriticalException(3);
            }
            time = System.nanoTime() - time;
            System.out.println(" done after " + time + "ns");
        }

        // Process
        i = 1;
        long total = instructions.size();
        OUTER: while (!replays.keySet().containsAll(outputs.keySet())) {
            for (Instruction instruction : instructions) {
                if (replays.keySet().containsAll(instruction.getInputs())) {
                    System.out.print(String.format("Processing instruction %d of %d: ", i++, total));
                    System.out.print(StringUtils.join(instruction.getInputs(), ","));
                    System.out.print(" -> " + instruction + " -> ");
                    System.out.print(StringUtils.join(instruction.getOutputs(), ","));
                    long time = System.nanoTime();
                    try {
                        List<ReplayPart> in = new ArrayList<>();
                        List<ReplayPart> out = new ArrayList<>();

                        for (String inName : instruction.getInputs()) {
                            in.add(replays.get(inName));
                        }

                        instruction.perform(studio, in, out);

                        int j = 0;
                        for (String outName : instruction.getOutputs()) {
                            replays.put(outName, out.get(j++));
                        }
                    } catch (Throwable t) {
                        System.out.println("Exception while processing " + instruction);
                        t.printStackTrace();
                        throw new CriticalException(4);
                    }
                    time = System.nanoTime() - time;
                    System.out.println(" done after " + time + "ns");
                    for (String out : instruction.getOutputs()) {
                        ReplayPart part = replays.get(out).copy();
                        System.out.println(
                                "Got " + out + " of length " + part.length() + " (" + part.size() + " packets)");
                    }
                    instructions.remove(instruction);
                    continue OUTER;
                }
            }
            System.out.println("Cannot run all instructions with active config!");
            System.out.println("Instructions missing: " + StringUtils.join(instructions, ','));
            throw new CriticalException(5);
        }

        // Write outputs
        i = 1;
        for (Map.Entry<String, String> entry : outputs.entrySet()) {
            System.out.print(String.format("Writing output %d of %d...", i, outputs.size()));
            long time = System.nanoTime();
            try {
                if (entry.getValue().equals("x")) {
                    System.out.print("discarded...");
                } else {
                    ReplayPart replayPart = replays.get(entry.getKey());
                    Replay replay = replayPart instanceof Replay ? (Replay) replayPart
                            : studio.createReplay(replayPart);
                    // TODO: MetaData
                    replay.save(new File(entry.getValue()));
                }
            } catch (Throwable t) {
                System.out.println("Exception while writing output to " + entry.getValue());
                t.printStackTrace();
                throw new CriticalException(6);
            }
            time = System.nanoTime() - time;
            System.out.println(" done after " + time + "ns");
        }
    }

    private void assertPossible() {
        List<Instruction> instructions = new ArrayList<>(this.instructions);
        Set<String> replays = new HashSet<>(inputs.keySet());
        OUTER: while (!replays.containsAll(outputs.keySet())) {
            for (Instruction instruction : instructions) {
                if (replays.containsAll(instruction.getInputs())) {
                    replays.addAll(instruction.getOutputs());
                    instructions.remove(instruction);
                    continue OUTER;
                }
            }
            System.out.println("Cannot run all instructions with active config!");
            System.out.println("Instructions missing: " + StringUtils.join(instructions, ','));
            throw new CriticalException(5);
        }
    }

    public void parseConfig(Studio studio, String line, String[] args) {
        String ios = line.substring(1, line.indexOf(')'));
        String instructions = line.substring(line.indexOf(')') + 2, line.length() - 1);

        int arg = 0;
        try {
            for (String ioi : ios.split(",")) {
                String io = ioi.substring(1);
                if (ioi.charAt(0) == '<') {
                    inputs.put(io, args[arg++]);
                } else if (ioi.charAt(0) == '>') {
                    outputs.put(io, args[arg++]);
                } else if (ioi.charAt(0) == '-') {
                    pipes.add(io);
                } else {
                    throw new IllegalArgumentException("Config input/output is invalid: " + ioi);
                }
            }
        } catch (IndexOutOfBoundsException e) {
            System.out.println("Insufficient input/output files. Need at least " + arg);
            throw new CriticalException(7);
        }

        for (String instructionStr : instructions.split(Pattern.quote(")("))) {
            String ins = instructionStr;
            List<String> inputs = new ArrayList<>();
            List<String> outputs = new ArrayList<>();

            // Read inputs
            OUTER: while (true) {
                for (int i = 0; i < ins.length(); i++) {
                    char c = ins.charAt(i);
                    if (!Character.isAlphabetic(c)) {
                        inputs.add(ins.substring(0, i));
                        if (c == ',') {
                            ins = ins.substring(i + 1);
                            continue OUTER;
                        } else {
                            ins = ins.substring(i);
                            break OUTER;
                        }
                    }
                }
                throw new IllegalArgumentException("Config input/output is invalid: " + instructionStr);
            }

            // Read outputs
            OUTER: while (true) {
                for (int i = ins.length() - 1; i >= 0; i--) {
                    char c = ins.charAt(i);
                    if (!Character.isAlphabetic(c)) {
                        outputs.add(ins.substring(i + 1));
                        if (c == ',') {
                            ins = ins.substring(0, i);
                            continue OUTER;
                        } else {
                            ins = ins.substring(0, i + 1);
                            break OUTER;
                        }
                    }
                }
                throw new IllegalArgumentException("Config instruction input/output is invalid: " + instructionStr);
            }
            Collections.reverse(outputs);

            String options = ins.length() > 1 ? ins.substring(1, ins.length() - 1) : "";
            Instruction instruction;
            switch (ins.charAt(0)) {
            case '|': // split
                Collection<Long> splitAt = Collections2.transform(Arrays.asList(options.split(",")),
                        this::timeStampToMillis);
                instruction = new SplitInstruction(Longs.toArray(splitAt));
                break;
            case '&': // append
                instruction = new AppendInstruction();
                break;
            case '[': // squash
                instruction = new SquashInstruction(studio);
                break;
            case ':': // copy
                instruction = new CopyInstruction();
                break;
            case '>': // filter
                String filterName;
                JsonObject filterOptions;
                if (options.contains(",")) {
                    String[] parts = options.split(",", 2);
                    filterName = parts[0];
                    filterOptions = new JsonParser().parse(parts[1]).getAsJsonObject();
                } else {
                    filterName = options;
                    filterOptions = new JsonObject();
                }
                Filter filter = studio.loadFilter(filterName);
                if (filter == null) {
                    throw new IllegalStateException("Filter not found: " + filterName);
                }
                instruction = new FilterInstruction(studio, filter, filterOptions);
                break;
            default:
                throw new IllegalArgumentException("Config instruction is unknown: " + instructionStr);
            }

            instruction.getInputs().addAll(inputs);
            instruction.getOutputs().addAll(outputs);

            this.instructions.add(instruction);
        }
    }

    public void parseConfig(Studio studio, JsonObject root) {
        JsonArray instructions = root.getAsJsonArray("Instructions");
        for (JsonElement e : instructions) {
            JsonObject o = e.getAsJsonObject();
            Instruction instruction;
            switch (o.get("Name").getAsString().toLowerCase()) {
            case "split":
                if (o.get("at").isJsonArray()) {
                    List<Long> at = new ArrayList<>();
                    Iterables.addAll(at,
                            Iterables.transform(o.getAsJsonArray("at"), (e1) -> timeStampToMillis(e1.toString())));
                    instruction = new SplitInstruction(Longs.toArray(at));
                } else {
                    instruction = new SplitInstruction(timeStampToMillis(o.get("at").toString()));
                }
                break;
            case "append":
                instruction = new AppendInstruction();
                break;
            case "squash":
                instruction = new SquashInstruction(studio);
                break;
            case "copy":
                instruction = new CopyInstruction();
                break;
            case "filter":
                Filter filter = studio.loadFilter(o.get("Filter").toString());
                instruction = new FilterInstruction(studio, filter, o.getAsJsonObject("Config"));
                break;
            default:
                System.out.println("Warning: Unrecognized instruction in json config: " + o.get("Name"));
                continue;
            }

            JsonElement inputs = o.get("Inputs");
            if (inputs.isJsonArray()) {
                for (JsonElement e1 : inputs.getAsJsonArray()) {
                    instruction.getInputs().add(e1.getAsString());
                }
            } else {
                instruction.getInputs().add(inputs.getAsString());
            }

            JsonElement outputs = o.get("Outputs");
            if (outputs.isJsonArray()) {
                for (JsonElement e1 : outputs.getAsJsonArray()) {
                    instruction.getOutputs().add(e1.getAsString());
                }
            } else {
                instruction.getOutputs().add(outputs.getAsString());
            }

            this.instructions.add(instruction);
        }

        // Get all inputs
        JsonObject inputs = root.getAsJsonObject("Inputs");
        for (Map.Entry<String, JsonElement> e : inputs.entrySet()) {
            this.inputs.put(e.getKey(), e.getValue().getAsString());
        }

        // Get all outputs
        JsonObject outputs = root.getAsJsonObject("Outputs");
        for (Map.Entry<String, JsonElement> e : outputs.entrySet()) {
            this.outputs.put(e.getKey(), e.getValue().getAsString());
        }

        // Calculate all pipes
        for (Instruction instruction : this.instructions) {
            pipes.addAll(instruction.getInputs());
            pipes.addAll(instruction.getOutputs());
        }
        pipes.removeAll(this.inputs.keySet());
        pipes.removeAll(this.outputs.keySet());
    }

    private long timeStampToMillis(String string) {
        try {
            return Long.parseLong(string);
        } catch (NumberFormatException e) {
            long time = 0;
            int hIndex = string.indexOf('h');
            if (hIndex != -1) {
                time += 3600000 * Integer.parseInt(string.substring(0, hIndex));
                if (string.length() - 1 > hIndex) {
                    string = string.substring(hIndex + 1);
                }
            }
            int mIndex = string.indexOf('m');
            if (mIndex != -1) {
                time += 60000 * Integer.parseInt(string.substring(0, mIndex));
                if (string.length() - 1 > mIndex) {
                    string = string.substring(mIndex + 1);
                }
            }
            int sIndex = string.indexOf('s');
            if (sIndex != -1) {
                time += 1000 * Integer.parseInt(string.substring(0, sIndex));
                if (string.length() - 1 > sIndex) {
                    string = string.substring(sIndex + 1);
                }
            }
            int msIndex = string.indexOf("ms");
            if (msIndex != -1) {
                time += Integer.parseInt(string.substring(0, msIndex));
            }
            return time;
        }
    }

    private interface Instruction {
        List<String> getInputs();

        List<String> getOutputs();

        void perform(Studio studio, List<ReplayPart> inputs, List<ReplayPart> outputs);
    }

    private static abstract class AbstractInstruction implements Instruction {
        private List<String> inputs = new ArrayList<>();
        private List<String> outputs = new ArrayList<>();

        @Override
        public String toString() {
            return getClass().getSimpleName();
        }

        public List<String> getInputs() {
            return this.inputs;
        }

        public List<String> getOutputs() {
            return this.outputs;
        }
    }

    private static class SplitInstruction extends AbstractInstruction {
        private final long[] splitAt;

        public SplitInstruction(long... splitAt) {
            this.splitAt = splitAt;
        }

        @Override
        public void perform(Studio studio, List<ReplayPart> inputs, List<ReplayPart> outputs) {
            ReplayPart input = inputs.get(0);
            long from = 0;
            for (long to : splitAt) {
                outputs.add(input.copyOf(from, to - 1));
                from = to;
            }
            outputs.add(input.copyOf(from));
        }
    }

    private static class AppendInstruction extends AbstractInstruction {
        @Override
        public void perform(Studio studio, List<ReplayPart> inputs, List<ReplayPart> outputs) {
            ReplayPart result = null;
            for (ReplayPart input : inputs) {
                if (result == null) {
                    result = input.copy();
                } else {
                    long first = input.size() == 0 ? 0 : input.iterator().next().getTime();
                    result.addAt(result.length() - first, input);
                }
            }
            outputs.add(result);
        }
    }

    private static class SquashInstruction extends FilterInstruction {
        public SquashInstruction(Studio studio) {
            super(studio, studio.loadFilter("squash"), new JsonObject());
        }
    }

    private static class CopyInstruction extends AbstractInstruction {
        @Override
        public void perform(Studio studio, List<ReplayPart> inputs, List<ReplayPart> outputs) {
            for (ReplayPart p : inputs) {
                outputs.add(p.copy());
            }
        }
    }

    private static class FilterInstruction extends AbstractInstruction {
        private final Filter filter;

        public FilterInstruction(Studio studio, Filter filter, JsonObject config) {
            this.filter = filter;
            filter.init(studio, config);
        }

        @Override
        public void perform(Studio studio, List<ReplayPart> inputs, List<ReplayPart> outputs) {
            outputs.add(filter.apply(inputs.get(0).copy()));
        }

        @Override
        public String toString() {
            return super.toString() + " (" + filter.getName() + ")";
        }
    }

    public static class CriticalException extends RuntimeException {
        private final int returnCode;

        public CriticalException(int returnCode) {
            this.returnCode = returnCode;
        }

        public int getReturnCode() {
            return returnCode;
        }
    }
}