io.github.mike10004.harreplay.exec.HarReplayMain Maven / Gradle / Ivy
package io.github.mike10004.harreplay.exec;
import io.github.mike10004.subprocess.ProcessMonitor;
import io.github.mike10004.subprocess.ScopedProcessTracker;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.gson.Gson;
import com.opencsv.CSVReader;
import com.browserup.harreader.HarReader;
import com.browserup.harreader.HarReaderException;
import com.browserup.harreader.HarReaderMode;
import com.browserup.harreader.model.Har;
import com.browserup.harreader.model.HarEntry;
import io.github.mike10004.harreplay.ReplayManager;
import io.github.mike10004.harreplay.ReplayServerConfig;
import io.github.mike10004.harreplay.ReplaySessionConfig;
import io.github.mike10004.harreplay.ReplaySessionControl;
import io.github.mike10004.harreplay.exec.ChromeBrowserSupport.OutputDestination;
import io.github.mike10004.harreplay.exec.HarInfoDumper.SummaryDumper;
import io.github.mike10004.harreplay.exec.HarInfoDumper.TerseDumper;
import io.github.mike10004.harreplay.exec.HarInfoDumper.VerboseDumper;
import io.github.mike10004.harreplay.vhsimpl.HarReaderFactory;
import io.github.mike10004.harreplay.vhsimpl.VhsReplayManager;
import io.github.mike10004.harreplay.vhsimpl.VhsReplayManagerConfig;
import joptsimple.NonOptionArgumentSpec;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.net.ServerSocket;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
public class HarReplayMain {
private static final Logger log = LoggerFactory.getLogger(HarReplayMain.class);
static final String OPT_NOTIFY = "notify";
static final String OPT_SCRATCH_DIR = "scratch-dir";
static final String OPT_PORT = "port";
static final String OPT_BROWSER = "browser";
static final String OPT_BROWSER_ARGS = "browser-args";
static final String OPT_REPLAY_CONFIG = "config";
static final String OPT_ECHO_BROWSER_OUTPUT = "echo-browser-output";
static final String OPT_HAR_READER_BEHAVIOR = "har-reader-behavior";
static final String OPT_HAR_READER_MODE = "har-reader-mode";
static final String OPT_PRINT = "print";
static final String OPT_VERSION = "version";
static final String OPT_HELP = "help";
static final String OPT_ONLY_PRINT = "only-print";
static final String OPT_PRINT_WITH_CONTENT = "content-dir";
static final Charset NOTIFY_FILE_CHARSET = StandardCharsets.US_ASCII;
private final OptionParser parser;
private final OptionSpec notifySpec;
private final NonOptionArgumentSpec harFileSpec;
private final OptionSpec portSpec;
private final OptionSpec scratchDirSpec;
private final OptionSpec browserSpec;
private final OptionSpec browserArgsSpec;
private final OptionSpec harDumpStyleSpec;
private final OptionSpec replayConfigSpec;
private final OptionSpec harReaderBehaviorSpec;
private final OptionSpec harReaderModeSpec;
public HarReplayMain() {
this(new OptionParser());
}
@VisibleForTesting
HarReplayMain(OptionParser parser) throws UsageException {
this.parser = requireNonNull(parser, "parser");
parser.formatHelpWith(new CustomHelpFormatter());
parser.acceptsAll(Arrays.asList("h", OPT_HELP), "print help and exit").forHelp();
parser.acceptsAll(Arrays.asList("V", OPT_VERSION), "print version and exit");
parser.acceptsAll(Arrays.asList("t", OPT_ONLY_PRINT), "only print content (do not start server)");
parser.accepts(OPT_ECHO_BROWSER_OUTPUT, "with --browser, print browser output to console");
parser.accepts(OPT_PRINT_WITH_CONTENT, "with --print=csv, write request/response content to DIR")
.withRequiredArg().ofType(File.class).describedAs("DIR");
harFileSpec = parser.nonOptions("har file").ofType(File.class).describedAs("FILE");
notifySpec = parser.accepts(OPT_NOTIFY, "notify that server is up by printing listening port to file")
.withRequiredArg().ofType(File.class);
portSpec = parser.acceptsAll(Arrays.asList("p", "P", OPT_PORT), "port to listen on")
.withRequiredArg().ofType(Integer.class)
.describedAs("PORT");
scratchDirSpec = parser.acceptsAll(Arrays.asList("d", OPT_SCRATCH_DIR), "scratch directory to use")
.withRequiredArg().ofType(File.class)
.describedAs("DIRNAME");
browserSpec = parser.acceptsAll(Arrays.asList("b", OPT_BROWSER), "launch browser configured for replay server; only 'chrome' is supported")
.withRequiredArg().ofType(Browser.class)
.describedAs("BROWSER");
browserArgsSpec = parser.acceptsAll(Arrays.asList("a", OPT_BROWSER_ARGS), "with --browser, add more arguments to browser command line; use CSV syntax for multiple args")
.withRequiredArg().ofType(String.class)
.describedAs("ARGS");
harDumpStyleSpec = parser.acceptsAll(Collections.singletonList(OPT_PRINT), "print har content (choices: " + HarPrintStyle.describeChoices() + ")")
.withRequiredArg().ofType(HarPrintStyle.class)
.describedAs("STYLE")
.defaultsTo(HarPrintStyle.summary);
replayConfigSpec = parser.acceptsAll(Arrays.asList("f", OPT_REPLAY_CONFIG), "specify replay config file")
.withRequiredArg().ofType(File.class);
harReaderBehaviorSpec = parser.accepts(OPT_HAR_READER_BEHAVIOR, "set har reader behavior (EASIER or STOCK)")
.withRequiredArg().ofType(HarReaderBehavior.class).defaultsTo(HarReaderBehavior.DEFAULT);
harReaderModeSpec = parser.accepts(OPT_HAR_READER_MODE, "set har reader mode (STRICT or LAX)")
.withRequiredArg().ofType(HarReaderMode.class).defaultsTo(HarReaderMode.STRICT);
}
private ReplayManager createReplayManager(OptionSet optionSet) {
HarReaderBehavior behavior = (HarReaderBehavior) optionSet.valueOf(OPT_HAR_READER_BEHAVIOR);
HarReaderMode mode = (HarReaderMode) optionSet.valueOf(OPT_HAR_READER_MODE);
VhsReplayManagerConfig.Builder b = VhsReplayManagerConfig.builder()
.harReaderFactory(behavior.getFactory())
.harReaderMode(mode);
VhsReplayManagerConfig vhsConfig = b.build();
return new VhsReplayManager(vhsConfig);
}
protected Har readHarFile(OptionSet options, File harFile) throws IOException, HarReaderException {
HarReaderBehavior harReaderBehavior = harReaderBehaviorSpec.value(options);
HarReaderMode harReaderMode = harReaderModeSpec.value(options);
return readHarFile(harFile, harReaderBehavior, harReaderMode);
}
@SuppressWarnings("RedundantThrows")
protected static Har readHarFile(File harFile, HarReaderBehavior harReaderBehavior, HarReaderMode harReaderMode) throws IOException, HarReaderException {
HarReaderFactory harReaderFactory = harReaderBehavior.getFactory();
HarReader harReader = harReaderFactory.createReader();
return harReader.readFromFile(harFile, harReaderMode);
}
protected List readHarEntries(OptionSet options, File harFile) throws IOException, HarReaderException {
Har har = readHarFile(options, harFile);
return har.getLog().getEntries();
}
protected Iterable tokenize(@Nullable String value) {
if (value == null) {
return ImmutableList.of();
}
List args = new ArrayList<>();
try (CSVReader reader = new CSVReader(new StringReader(value))) {
List rows = reader.readAll();
rows.forEach(row -> args.addAll(Arrays.asList(row)));
} catch (IOException e) {
log.warn("failed to tokenize arguments from " + value, e);
}
return args;
}
protected void runServer(OptionSet optionSet, ReplaySessionConfig sessionConfig) throws IOException {
HostAndPort replayServerAddress = HostAndPort.fromParts("localhost", sessionConfig.port);
ReplayManager manager = createReplayManager(optionSet);
try (ReplaySessionControl ignore = manager.start(sessionConfig);
ScopedProcessTracker processTracker = new ProcessTrackerWithShutdownHook(Runtime.getRuntime())) {
maybeNotify(sessionConfig, optionSet.valueOf(notifySpec));
Browser browser = optionSet.valueOf(browserSpec);
if (browser != null) {
Iterable browserArgs = tokenize(optionSet.valueOf(browserArgsSpec));
//noinspection unused // TODO: provide an alternate method to initate orderly shutdown using this monitor
ProcessMonitor, ?> monitor = browser.getSupport(optionSet)
.prepare(sessionConfig.scratchDir)
.launch(replayServerAddress, browserArgs, processTracker);
}
sleepForever();
}
}
protected void operate(OptionSet optionSet) throws IOException {
try (CloseableWrapper sessionConfigWrapper = createReplaySessionConfig(optionSet)) {
ReplaySessionConfig sessionConfig = sessionConfigWrapper.getWrapped();
HarPrintStyle harDumpStyle = optionSet.valueOf(harDumpStyleSpec);
try {
harDumpStyle.getDumper(optionSet).dump(readHarEntries(optionSet, sessionConfig.harFile), System.out);
} catch (HarReaderException e) {
System.err.format("har-replay: failed to read from har file: %s%n", e.getMessage());
}
if (optionSet.has(OPT_ONLY_PRINT)) {
return;
}
runServer(optionSet, sessionConfig);
}
}
int main0(String[] args) throws IOException {
try {
OptionSet optionSet = parser.parse(args);
if (optionSet.has(OPT_HELP)) {
parser.printHelpOn(System.out);
return 0;
}
if (optionSet.has(OPT_VERSION)) {
printVersion(System.out);
return 0;
}
operate(optionSet);
} catch (UsageException | joptsimple.OptionException e) {
System.err.format("har-replay: %s%n", e.getMessage());
System.err.format("har-replay: use --help to print options%n");
return 1;
}
return 0;
}
static Properties loadMavenProperties() {
Properties p = new Properties();
URL resource = HarReplayMain.class.getResource("/har-replay-exec/maven.properties");
if (resource == null) {
log.info("maven.properties is not present on classpath");
return p;
}
try (InputStream in = resource.openStream()) {
p.load(in);
} catch (IOException e) {
log.warn("failed to read from " + resource, e);
}
return p;
}
static final String DEFAULT_VERSION = "version_unknown";
@SuppressWarnings("SameParameterValue")
protected void printVersion(PrintStream out) {
Properties p = loadMavenProperties();
String name = p.getProperty("project.parent.name", "har-replay");
String version = p.getProperty("project.version", DEFAULT_VERSION);
URL location = getClass().getProtectionDomain().getCodeSource().getLocation();
out.format("%s %s (in %s)%n", name, version, location);
}
protected void sleepForever() {
Uninterruptibles.sleepUninterruptibly(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
}
protected void maybeNotify(ReplaySessionConfig sessionConfig, @Nullable File notifyFile) throws IOException {
if (notifyFile != null) {
Files.asCharSink(notifyFile, NOTIFY_FILE_CHARSET).write(String.valueOf(sessionConfig.port));
}
}
protected int findUnusedPort() throws IOException {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
}
}
protected CloseableWrapper createReplaySessionConfig(OptionSet optionSet) throws IOException {
File scratchDir = optionSet.valueOf(scratchDirSpec);
List cleanups = new ArrayList<>();
if (scratchDir == null) {
Path scratchDirPath = java.nio.file.Files.createTempDirectory("har-replay-temporary");
cleanups.add(() -> {
try {
FileUtils.forceDelete(scratchDirPath.toFile());
} catch (IOException e) {
if (scratchDirPath.toFile().exists()) {
log.warn("failed to delete scratch directory " + scratchDirPath, e);
}
}
});
scratchDir = scratchDirPath.toFile();
}
Integer port = optionSet.valueOf(portSpec);
if (port == null) {
port = findUnusedPort();
}
File harFile = optionSet.valueOf(harFileSpec);
if (harFile == null) {
throw new UsageException("har file must be specified as positional argument");
}
ReplayServerConfig serverConfig = buildReplayServerConfig(optionSet);
ReplaySessionConfig config = ReplaySessionConfig.builder(scratchDir.toPath())
.config(serverConfig)
.port(port)
.build(harFile);
return new CloseableWrapper() {
@Override
public ReplaySessionConfig getWrapped() {
return config;
}
@Override
public void close() {
cleanups.forEach(Runnable::run);
}
};
}
protected Gson createReplayServerConfigGson() {
return createDefaultReplayServerConfigGson();
}
protected static Gson createDefaultReplayServerConfigGson() {
return ReplayServerConfig.createSerialist();
}
protected ReplayServerConfig buildReplayServerConfig(OptionSet optionSet) throws IOException {
File replayConfigFile = replayConfigSpec.value(optionSet);
if (replayConfigFile != null) {
try (Reader reader = Files.asCharSource(replayConfigFile, StandardCharsets.UTF_8).openStream()) {
return createReplayServerConfigGson().fromJson(reader, ReplayServerConfig.class);
}
} else {
return ReplayServerConfig.empty();
}
}
@SuppressWarnings("unused")
private static class UsageException extends RuntimeException {
public UsageException(String message) {
super(message);
}
public UsageException(String message, Throwable cause) {
super(message, cause);
}
public UsageException(Throwable cause) {
super(cause);
}
}
public static void main(String[] args) throws Exception {
int exitCode = new HarReplayMain().main0(args);
System.exit(exitCode);
}
protected interface CloseableWrapper extends Closeable {
T getWrapped();
}
public enum Browser {
chrome;
BrowserSupport getSupport(OptionSet options) {
switch (this) {
case chrome:
return new ChromeBrowserSupport(
options.has(OPT_ECHO_BROWSER_OUTPUT) ? OutputDestination.CONSOLE : OutputDestination.FILES);
}
throw new IllegalStateException("not handled: " + this);
}
}
public enum HarReaderBehavior {
EASIER,
STOCK;
public static final HarReaderBehavior DEFAULT = EASIER;
public HarReaderFactory getFactory() {
switch (this) {
case EASIER:
return HarReaderFactory.easier();
case STOCK:
return HarReaderFactory.stock();
default:
throw new IllegalStateException("unhandled: " + this);
}
}
}
public enum HarPrintStyle {
silent,
terse,
summary,
csv,
verbose;
HarInfoDumper getDumper(OptionSet optionSet) {
switch (this) {
case silent: return HarInfoDumper.silent();
case terse: return new TerseDumper();
case summary: return new SummaryDumper();
case verbose: return new VerboseDumper();
case csv: return HarInfoDumper.CsvDumper.makeContentWritingInstance((File) optionSet.valueOf(OPT_PRINT_WITH_CONTENT));
}
throw new IllegalStateException("not handled: " + this);
}
public static String describeChoices() {
return String.join(", ", Stream.of(values()).map(c -> String.format("'%s'", c.name())).collect(Collectors.toList()));
}
}
private static class ProcessTrackerWithShutdownHook extends ScopedProcessTracker {
public ProcessTrackerWithShutdownHook(Runtime runtime) {
addShutdownHook(runtime);
}
private void addShutdownHook(Runtime runtime) {
runtime.addShutdownHook(new Thread(this::destroyAll));
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy