diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java
new file mode 100644
index 0000000000..19391898b5
--- /dev/null
+++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java
@@ -0,0 +1,91 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.apitest.linux;
+
+import java.io.File;
+import java.io.IOException;
+
+import lombok.extern.slf4j.Slf4j;
+
+import static bisq.apitest.linux.BashCommand.isAlive;
+import static java.lang.String.format;
+
+
+
+import bisq.apitest.config.ApiTestConfig;
+
+@Slf4j
+abstract class AbstractLinuxProcess implements LinuxProcess {
+
+ protected final String name;
+
+ protected long pid;
+
+ protected final ApiTestConfig config;
+
+ public AbstractLinuxProcess(String name, ApiTestConfig config) {
+ this.name = name;
+ this.config = config;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ public void verifyBitcoinConfig() {
+ verifyBitcoinConfig(false);
+ }
+
+ public void verifyBitcoinConfig(boolean verbose) {
+ if (verbose)
+ log.info(format("Checking bitcoind env...%n"
+ + "\t%-20s%s%n\t%-20s%s%n\t%-20s%s%n\t%-20s%s%n\t%-20s%s",
+ "berkeleyDbLibPath", config.berkeleyDbLibPath,
+ "bitcoinPath", config.bitcoinPath,
+ "bitcoinDatadir", config.bitcoinDatadir,
+ "bitcoin.conf", config.bitcoinDatadir + "/bitcoin.conf",
+ "blocknotify", config.bitcoinDatadir + "/blocknotify"));
+
+ File berkeleyDbLibPath = new File(config.berkeleyDbLibPath);
+ if (!berkeleyDbLibPath.exists() || !berkeleyDbLibPath.canExecute())
+ throw new IllegalStateException(berkeleyDbLibPath + "cannot be found or executed");
+
+ File bitcoindExecutable = new File(config.bitcoinPath);
+ if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute())
+ throw new IllegalStateException(bitcoindExecutable + "cannot be found or executed");
+
+ File bitcoindDatadir = new File(config.bitcoinDatadir);
+ if (!bitcoindDatadir.exists() || !bitcoindDatadir.canWrite())
+ throw new IllegalStateException(bitcoindDatadir + "cannot be found or written to");
+
+ File bitcoinConf = new File(bitcoindDatadir, "bitcoin.conf");
+ if (!bitcoinConf.exists() || !bitcoinConf.canRead())
+ throw new IllegalStateException(bitcoindDatadir + "cannot be found or read");
+
+ File blocknotify = new File(bitcoindDatadir, "blocknotify");
+ if (!blocknotify.exists() || !blocknotify.canExecute())
+ throw new IllegalStateException(bitcoindDatadir + "cannot be found or executed");
+ }
+
+ public void verifyBitcoindRunning() throws IOException, InterruptedException {
+ long bitcoindPid = BashCommand.getPid("bitcoind");
+ if (bitcoindPid < 0 || !isAlive(bitcoindPid))
+ throw new IllegalStateException("Bitcoind not running");
+ }
+}
diff --git a/apitest/src/main/java/bisq/apitest/linux/BashCommand.java b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java
new file mode 100644
index 0000000000..2eaa0b9f8e
--- /dev/null
+++ b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java
@@ -0,0 +1,149 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.apitest.linux;
+
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+
+import org.jetbrains.annotations.NotNull;
+
+import static java.lang.management.ManagementFactory.getRuntimeMXBean;
+
+
+
+import bisq.apitest.config.ApiTestConfig;
+
+@Slf4j
+public class BashCommand {
+
+ private int exitStatus = -1;
+ private String output;
+ private String error;
+
+ private final String command;
+ private final int numResponseLines;
+
+ public BashCommand(String command) {
+ this(command, 0);
+ }
+
+ public BashCommand(String command, int numResponseLines) {
+ this.command = command;
+ this.numResponseLines = numResponseLines; // only want the top N lines of output
+ }
+
+ public BashCommand run() throws IOException, InterruptedException {
+ SystemCommandExecutor commandExecutor = new SystemCommandExecutor(tokenizeSystemCommand());
+ exitStatus = commandExecutor.execCommand();
+
+ // Get the error status and stderr from system command.
+ StringBuilder stderr = commandExecutor.getStandardErrorFromCommand();
+ if (stderr.length() > 0)
+ error = stderr.toString();
+
+ if (exitStatus != 0)
+ return this;
+
+ // Format and cache the stdout from system command.
+ StringBuilder stdout = commandExecutor.getStandardOutputFromCommand();
+ String[] rawLines = stdout.toString().split("\n");
+ StringBuilder truncatedLines = new StringBuilder();
+ int limit = numResponseLines > 0 ? Math.min(numResponseLines, rawLines.length) : rawLines.length;
+ for (int i = 0; i < limit; i++) {
+ String line = rawLines[i].length() >= 220 ? rawLines[i].substring(0, 220) + " ..." : rawLines[i];
+ truncatedLines.append(line).append((i < limit - 1) ? "\n" : "");
+ }
+ output = truncatedLines.toString();
+ return this;
+ }
+
+ public String getCommand() {
+ return this.command;
+ }
+
+ public int getExitStatus() {
+ return this.exitStatus;
+ }
+
+ // TODO return Optional
+ public String getOutput() {
+ return this.output;
+ }
+
+ // TODO return Optional
+ public String getError() {
+ return this.error;
+ }
+
+ @NotNull
+ private List tokenizeSystemCommand() {
+ return new ArrayList<>() {{
+ add(ApiTestConfig.BASH_PATH_VALUE);
+ add("-c");
+ add(command);
+ }};
+ }
+
+ @SuppressWarnings("unused")
+ // Convenience method for getting system load info.
+ public static String printSystemLoadString(Exception tracingException) throws IOException, InterruptedException {
+ StackTraceElement[] stackTraceElement = tracingException.getStackTrace();
+ StringBuilder stackTraceBuilder = new StringBuilder(tracingException.getMessage()).append("\n");
+ int traceLimit = Math.min(stackTraceElement.length, 4);
+ for (int i = 0; i < traceLimit; i++) {
+ stackTraceBuilder.append(stackTraceElement[i]).append("\n");
+ }
+ stackTraceBuilder.append("...");
+ log.info(stackTraceBuilder.toString());
+ BashCommand cmd = new BashCommand("ps -aux --sort -rss --headers", 2).run();
+ return cmd.getOutput() + "\n"
+ + "System load: Memory (MB): " + getUsedMemoryInMB() + " / No. of threads: " + Thread.activeCount()
+ + " JVM uptime (ms): " + getRuntimeMXBean().getUptime();
+ }
+
+ public static long getUsedMemoryInMB() {
+ Runtime runtime = Runtime.getRuntime();
+ long free = runtime.freeMemory() / 1024 / 1024;
+ long total = runtime.totalMemory() / 1024 / 1024;
+ return total - free;
+ }
+
+ public static long getPid(String processName) throws IOException, InterruptedException {
+ String psCmd = "ps aux | pgrep " + processName + " | grep -v grep";
+ String psCmdOutput = new BashCommand(psCmd).run().getOutput();
+ if (psCmdOutput == null || psCmdOutput.isEmpty())
+ return -1;
+
+ return Long.parseLong(psCmdOutput);
+ }
+
+ @SuppressWarnings("unused")
+ public static BashCommand grep(String processName) throws IOException, InterruptedException {
+ String c = "ps -aux | grep " + processName + " | grep -v grep";
+ return new BashCommand(c).run();
+ }
+
+ public static boolean isAlive(long pid) throws IOException, InterruptedException {
+ String isAliveScript = "if ps -p " + pid + " > /dev/null; then echo true; else echo false; fi";
+ return new BashCommand(isAliveScript).run().getOutput().equals("true");
+ }
+}
diff --git a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java
new file mode 100644
index 0000000000..2ed3d3540f
--- /dev/null
+++ b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java
@@ -0,0 +1,257 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.apitest.linux;
+
+import java.nio.file.Paths;
+
+import java.io.File;
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+
+import static bisq.apitest.linux.BashCommand.isAlive;
+import static java.lang.String.format;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+
+
+import bisq.apitest.config.ApiTestConfig;
+import bisq.apitest.config.BisqAppConfig;
+import bisq.daemon.app.BisqDaemonMain;
+
+/**
+ * Runs a regtest/dao Bisq application instance in the background.
+ */
+@Slf4j
+public class BisqApp extends AbstractLinuxProcess implements LinuxProcess {
+
+ private final BisqAppConfig bisqAppConfig;
+ private final String baseCurrencyNetwork;
+ private final String genesisTxId;
+ private final int genesisBlockHeight;
+ private final String seedNodes;
+ private final boolean daoActivated;
+ private final boolean fullDaoNode;
+ private final boolean useLocalhostForP2P;
+ public final boolean useDevPrivilegeKeys;
+ private final String findBisqPidScript;
+ private final List startupExceptions;
+
+ public BisqApp(BisqAppConfig bisqAppConfig, ApiTestConfig config) {
+ super(bisqAppConfig.appName, config);
+ this.bisqAppConfig = bisqAppConfig;
+ this.baseCurrencyNetwork = "BTC_REGTEST";
+ this.genesisTxId = "30af0050040befd8af25068cc697e418e09c2d8ebd8d411d2240591b9ec203cf";
+ this.genesisBlockHeight = 111;
+ this.seedNodes = "localhost:2002";
+ this.daoActivated = true;
+ this.fullDaoNode = true;
+ this.useLocalhostForP2P = true;
+ this.useDevPrivilegeKeys = true;
+ this.findBisqPidScript = config.userDir + "/apitest/scripts/get-bisq-pid.sh";
+ this.startupExceptions = new ArrayList<>();
+ }
+
+ @Override
+ public void start() throws InterruptedException, IOException {
+ try {
+ if (config.runSubprojectJars)
+ runJar(); // run subproject/build/lib/*.jar (not full build)
+ else
+ runStartupScript(); // run bisq-* script for end to end test (default)
+ } catch (Throwable t) {
+ startupExceptions.add(t);
+ }
+ }
+
+ @Override
+ public long getPid() {
+ return this.pid;
+ }
+
+ @Override
+ public void shutdown() throws IOException, InterruptedException {
+ try {
+ log.info("Shutting down {} ...", bisqAppConfig.appName);
+ if (!isAlive(pid))
+ throw new IllegalStateException(format("%s already shut down", bisqAppConfig.appName));
+
+ String killCmd = "kill -15 " + pid;
+ if (new BashCommand(killCmd).run().getExitStatus() != 0)
+ throw new IllegalStateException(format("Could not shut down %s", bisqAppConfig.appName));
+
+ MILLISECONDS.sleep(4000); // allow it time to shutdown
+ log.info("{} stopped", bisqAppConfig.appName);
+ } catch (Exception e) {
+ throw new IllegalStateException(format("Error shutting down %s", bisqAppConfig.appName), e);
+ } finally {
+ if (isAlive(pid))
+ //noinspection ThrowFromFinallyBlock
+ throw new IllegalStateException(format("%s shutdown did not work", bisqAppConfig.appName));
+ }
+ }
+
+ public void verifyAppNotRunning() throws IOException, InterruptedException {
+ long pid = findBisqAppPid();
+ if (pid >= 0)
+ throw new IllegalStateException(format("%s %s already running with pid %d",
+ bisqAppConfig.mainClassName, bisqAppConfig.appName, pid));
+ }
+
+ public void verifyAppDataDirInstalled() {
+ // If we're running an Alice or Bob daemon, make sure the dao-setup directory
+ // are installed.
+ switch (bisqAppConfig) {
+ case alicedaemon:
+ case alicedesktop:
+ case bobdaemon:
+ case bobdesktop:
+ File bisqDataDir = new File(config.rootAppDataDir, bisqAppConfig.appName);
+ if (!bisqDataDir.exists())
+ throw new IllegalStateException(format("Application dataDir %s/%s not found",
+ config.rootAppDataDir, bisqAppConfig.appName));
+ break;
+ default:
+ break;
+ }
+ }
+
+ public boolean hasStartupExceptions() {
+ return !startupExceptions.isEmpty();
+ }
+
+ public List getStartupExceptions() {
+ return startupExceptions;
+ }
+
+ // This is the non-default way of running a Bisq app (--runSubprojectJars=true).
+ // It runs a java cmd, and does not depend on a full build. Bisq jars are loaded
+ // from the :subproject/build/libs directories.
+ private void runJar() throws IOException, InterruptedException {
+ String java = getJavaExecutable().getAbsolutePath();
+ String classpath = System.getProperty("java.class.path");
+ String bisqCmd = getJavaOptsSpec()
+ + " " + java + " -cp " + classpath
+ + " " + bisqAppConfig.mainClassName
+ + " " + String.join(" ", getOptsList())
+ + " &"; // run in background without nohup
+ runBashCommand(bisqCmd);
+ }
+
+ // This is the default way of running a Bisq app (--runSubprojectJars=false).
+ // It runs a bisq-* startup script, and depends on a full build. Bisq jars
+ // are loaded from the root project's lib directory.
+ private void runStartupScript() throws IOException, InterruptedException {
+ String bisqCmd = getJavaOptsSpec()
+ + " " + config.userDir + "/" + bisqAppConfig.startupScript
+ + " " + String.join(" ", getOptsList())
+ + " &"; // run in background without nohup
+ runBashCommand(bisqCmd);
+ }
+
+ private void runBashCommand(String bisqCmd) throws IOException, InterruptedException {
+ String cmdDescription = config.runSubprojectJars
+ ? "java -> " + bisqAppConfig.mainClassName + " -> " + bisqAppConfig.appName
+ : bisqAppConfig.startupScript + " -> " + bisqAppConfig.appName;
+ BashCommand bashCommand = new BashCommand(bisqCmd);
+ log.info("Starting {} ...\n$ {}", cmdDescription, bashCommand.getCommand());
+ bashCommand.run();
+
+ if (bashCommand.getExitStatus() != 0)
+ throw new IllegalStateException(format("Error starting BisqApp\n%s\nError: %s",
+ bashCommand.getError()));
+
+ // Sometimes it takes a little extra time to find the linux process id.
+ // Wait up to two seconds before giving up and throwing an Exception.
+ for (int i = 0; i < 4; i++) {
+ pid = findBisqAppPid();
+ if (pid != -1)
+ break;
+
+ MILLISECONDS.sleep(500L);
+ }
+ if (!isAlive(pid))
+ throw new IllegalStateException(format("Error finding pid for %s", this.name));
+
+ log.info("{} running with pid {}", cmdDescription, pid);
+ log.info("Log {}", config.rootAppDataDir + "/" + bisqAppConfig.appName + "/bisq.log");
+ }
+
+ private long findBisqAppPid() throws IOException, InterruptedException {
+ // Find the pid of the java process by grepping for the mainClassName and appName.
+ String findPidCmd = findBisqPidScript + " " + bisqAppConfig.mainClassName + " " + bisqAppConfig.appName;
+ String psCmdOutput = new BashCommand(findPidCmd).run().getOutput();
+ return (psCmdOutput == null || psCmdOutput.isEmpty()) ? -1 : Long.parseLong(psCmdOutput);
+ }
+
+ private String getJavaOptsSpec() {
+ return "export JAVA_OPTS=" + bisqAppConfig.javaOpts + "; ";
+ }
+
+ private List getOptsList() {
+ return new ArrayList<>() {{
+ add("--appName=" + bisqAppConfig.appName);
+ add("--appDataDir=" + config.rootAppDataDir.getAbsolutePath() + "/" + bisqAppConfig.appName);
+ add("--nodePort=" + bisqAppConfig.nodePort);
+ add("--rpcBlockNotificationPort=" + bisqAppConfig.rpcBlockNotificationPort);
+ add("--rpcUser=" + config.bitcoinRpcUser);
+ add("--rpcPassword=" + config.bitcoinRpcPassword);
+ add("--rpcPort=" + config.bitcoinRpcPort);
+ add("--daoActivated=" + daoActivated);
+ add("--fullDaoNode=" + fullDaoNode);
+ add("--seedNodes=" + seedNodes);
+ add("--baseCurrencyNetwork=" + baseCurrencyNetwork);
+ add("--useDevPrivilegeKeys=" + useDevPrivilegeKeys);
+ add("--useLocalhostForP2P=" + useLocalhostForP2P);
+ switch (bisqAppConfig) {
+ case seednode:
+ break; // no extra opts needed for seed node
+ case arbdaemon:
+ case arbdesktop:
+ case alicedaemon:
+ case alicedesktop:
+ case bobdaemon:
+ case bobdesktop:
+ add("--genesisBlockHeight=" + genesisBlockHeight);
+ add("--genesisTxId=" + genesisTxId);
+ if (bisqAppConfig.mainClassName.equals(BisqDaemonMain.class.getName())) {
+ add("--apiPassword=" + config.apiPassword);
+ add("--apiPort=" + bisqAppConfig.apiPort);
+ }
+ break;
+ default:
+ throw new IllegalStateException("Unknown BisqAppConfig " + bisqAppConfig.name());
+ }
+ }};
+ }
+
+ private File getJavaExecutable() {
+ File javaHome = Paths.get(System.getProperty("java.home")).toFile();
+ if (!javaHome.exists())
+ throw new IllegalStateException(format("$JAVA_HOME not found, cannot run %s", bisqAppConfig.mainClassName));
+
+ File javaExecutable = Paths.get(javaHome.getAbsolutePath(), "bin", "java").toFile();
+ if (javaExecutable.exists() || javaExecutable.canExecute())
+ return javaExecutable;
+ else
+ throw new IllegalStateException("$JAVA_HOME/bin/java not found or executable");
+ }
+}
diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java
new file mode 100644
index 0000000000..2fd475cce7
--- /dev/null
+++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java
@@ -0,0 +1,166 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.apitest.linux;
+
+import java.io.IOException;
+
+import lombok.extern.slf4j.Slf4j;
+
+
+
+import bisq.apitest.config.ApiTestConfig;
+
+@Slf4j
+public class BitcoinCli extends AbstractLinuxProcess implements LinuxProcess {
+
+ private final String command;
+
+ private String commandWithOptions;
+ private String output;
+ private boolean error;
+
+ public BitcoinCli(ApiTestConfig config, String command) {
+ super("bitcoin-cli", config);
+ this.command = command;
+ this.error = false;
+ }
+
+ public BitcoinCli run() throws IOException, InterruptedException {
+ this.start();
+ return this;
+ }
+
+ public String getCommandWithOptions() {
+ return commandWithOptions;
+ }
+
+ public String getOutput() {
+ if (isError())
+ throw new IllegalStateException(output);
+
+ // Some responses are not in json format, such as what is returned by
+ // 'getnewaddress'. The raw output string is the value.
+
+ return output;
+ }
+
+ public String[] getOutputValueAsStringArray() {
+ if (isError())
+ throw new IllegalStateException(output);
+
+ if (!output.startsWith("[") && !output.endsWith("]"))
+ throw new IllegalStateException(output + "\nis not a json array");
+
+ String[] lines = output.split("\n");
+ String[] array = new String[lines.length - 2];
+ for (int i = 1; i < lines.length - 1; i++) {
+ array[i - 1] = lines[i].replaceAll("[^a-zA-Z0-9.]", "");
+ }
+
+ return array;
+ }
+
+ public String getOutputValueAsString(String key) {
+ if (isError())
+ throw new IllegalStateException(output);
+
+ // Some assumptions about bitcoin-cli json string parsing:
+ // Every multi valued, non-error bitcoin-cli response will be a json string.
+ // Every key/value in the json string will terminate with a newline.
+ // Most key/value lines in json strings have a ',' char in front of the newline.
+ // e.g., bitcoin-cli 'getwalletinfo' output:
+ // {
+ // "walletname": "",
+ // "walletversion": 159900,
+ // "balance": 527.49941568,
+ // "unconfirmed_balance": 0.00000000,
+ // "immature_balance": 5000.00058432,
+ // "txcount": 114,
+ // "keypoololdest": 1528018235,
+ // "keypoolsize": 1000,
+ // "keypoolsize_hd_internal": 1000,
+ // "paytxfee": 0.00000000,
+ // "hdseedid": "179b609a60c2769138844c3e36eb430fd758a9c6",
+ // "private_keys_enabled": true,
+ // "avoid_reuse": false,
+ // "scanning": false
+ // }
+
+ int keyIdx = output.indexOf("\"" + key + "\":");
+ int eolIdx = output.indexOf("\n", keyIdx);
+ String valueLine = output.substring(keyIdx, eolIdx); // "balance": 527.49941568,
+ String[] keyValue = valueLine.split(":");
+
+ // Remove all but alphanumeric chars and decimal points from the return value,
+ // including quotes around strings, and trailing commas.
+ // Adjustments will be necessary as we begin to work with more complex
+ // json values, such as arrays.
+ return keyValue[1].replaceAll("[^a-zA-Z0-9.]", "");
+ }
+
+ public boolean getOutputValueAsBoolean(String key) {
+ String valueStr = getOutputValueAsString(key);
+ return Boolean.parseBoolean(valueStr);
+ }
+
+
+ public int getOutputValueAsInt(String key) {
+ String valueStr = getOutputValueAsString(key);
+ return Integer.parseInt(valueStr);
+ }
+
+ public double getOutputValueAsDouble(String key) {
+ String valueStr = getOutputValueAsString(key);
+ return Double.parseDouble(valueStr);
+ }
+
+ public long getOutputValueAsLong(String key) {
+ String valueStr = getOutputValueAsString(key);
+ return Long.parseLong(valueStr);
+ }
+
+ public boolean isError() {
+ return error;
+ }
+
+ @Override
+ public void start() throws InterruptedException, IOException {
+ verifyBitcoinConfig(false);
+ verifyBitcoindRunning();
+ commandWithOptions = config.bitcoinPath + "/bitcoin-cli -regtest "
+ + " -rpcuser=" + config.bitcoinRpcUser
+ + " -rpcpassword=" + config.bitcoinRpcPassword
+ + " " + command;
+ output = new BashCommand(commandWithOptions).run().getOutput();
+ error = output.startsWith("error");
+ }
+
+ @Override
+ public long getPid() {
+ // We don't cache the pid. The bitcoin-cli will quickly return a
+ // response, including server error info if any.
+ throw new UnsupportedOperationException("getPid not supported");
+ }
+
+ @Override
+ public void shutdown() {
+ // We don't try to shutdown the bitcoin-cli. It will quickly return a
+ // response, including server error info if any.
+ throw new UnsupportedOperationException("shutdown not supported");
+ }
+}
diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java
new file mode 100644
index 0000000000..ac958431a4
--- /dev/null
+++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java
@@ -0,0 +1,91 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.apitest.linux;
+
+import java.io.IOException;
+
+import lombok.extern.slf4j.Slf4j;
+
+import static bisq.apitest.linux.BashCommand.isAlive;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+
+
+import bisq.apitest.config.ApiTestConfig;
+
+// Some cmds:
+// bitcoin-cli -regtest generatetoaddress 1 "2MyBq4jbtDF6CfKNrrQdp7qkRc8mKuCpKno"
+// bitcoin-cli -regtest getbalance
+// note: getbalance does not include immature coins (<100 blks deep)
+// bitcoin-cli -regtest getbalances
+// bitcoin-cli -regtest getrpcinfo
+
+@Slf4j
+public class BitcoinDaemon extends AbstractLinuxProcess implements LinuxProcess {
+
+ public BitcoinDaemon(ApiTestConfig config) {
+ super("bitcoind", config);
+ }
+
+ @Override
+ public void start() throws InterruptedException, IOException {
+ String bitcoindCmd = "export LD_LIBRARY_PATH=" + config.berkeleyDbLibPath + ";"
+ + " " + config.bitcoinPath + "/bitcoind"
+ + " -datadir=" + config.bitcoinDatadir
+ + " -daemon";
+
+ BashCommand cmd = new BashCommand(bitcoindCmd).run();
+ log.info("Starting ...\n$ {}", cmd.getCommand());
+
+ if (cmd.getExitStatus() != 0)
+ throw new IllegalStateException("Error starting bitcoind:\n" + cmd.getError());
+
+ pid = BashCommand.getPid("bitcoind");
+ if (!isAlive(pid))
+ throw new IllegalStateException("Error starting regtest bitcoind daemon:\n" + cmd.getCommand());
+
+ log.info("Running with pid {}", pid);
+ log.info("Log {}", config.bitcoinDatadir + "/regtest/debug.log");
+ }
+
+ @Override
+ public long getPid() {
+ return this.pid;
+ }
+
+ @Override
+ public void shutdown() throws IOException, InterruptedException {
+ try {
+ log.info("Shutting down bitcoind daemon...");
+ if (!isAlive(pid))
+ throw new IllegalStateException("bitcoind already shut down");
+
+ if (new BashCommand("killall bitcoind").run().getExitStatus() != 0)
+ throw new IllegalStateException("Could not shut down bitcoind; probably already stopped.");
+
+ MILLISECONDS.sleep(2000); // allow it time to shutdown
+ log.info("Stopped");
+ } catch (Exception e) {
+ throw new IllegalStateException("Error shutting down bitcoind", e);
+ } finally {
+ if (isAlive(pid))
+ //noinspection ThrowFromFinallyBlock
+ throw new IllegalStateException("bitcoind shutdown did not work");
+ }
+ }
+}
diff --git a/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java
new file mode 100644
index 0000000000..aa673c8803
--- /dev/null
+++ b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java
@@ -0,0 +1,30 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.apitest.linux;
+
+import java.io.IOException;
+
+public interface LinuxProcess {
+ void start() throws InterruptedException, IOException;
+
+ String getName();
+
+ long getPid();
+
+ void shutdown() throws IOException, InterruptedException;
+}
diff --git a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java
new file mode 100644
index 0000000000..ece5df1e1c
--- /dev/null
+++ b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java
@@ -0,0 +1,119 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.apitest.linux;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * This class can be used to execute a system command from a Java application.
+ * See the documentation for the public methods of this class for more
+ * information.
+ *
+ * Documentation for this class is available at this URL:
+ *
+ * http://devdaily.com/java/java-processbuilder-process-system-exec
+ *
+ * Copyright 2010 alvin j. alexander, devdaily.com.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser Public License for more details.
+ * You should have received a copy of the GNU Lesser Public License
+ * along with this program. If not, see .
+ *
+ * Please ee the following page for the LGPL license:
+ * http://www.gnu.org/licenses/lgpl.txt
+ *
+ */
+@Slf4j
+class SystemCommandExecutor {
+ private final List cmdOptions;
+ private final String sudoPassword;
+ private ThreadedStreamHandler inputStreamHandler;
+ private ThreadedStreamHandler errorStreamHandler;
+
+ /*
+ * Note: I've removed the other constructor that was here to support executing
+ * the sudo command. I'll add that back in when I get the sudo command
+ * working to the point where it won't hang when the given password is
+ * wrong.
+ */
+ public SystemCommandExecutor(final List cmdOptions) {
+ if (cmdOptions == null)
+ throw new NullPointerException("No command params specified.");
+
+ this.cmdOptions = cmdOptions;
+ this.sudoPassword = null;
+ }
+
+ // Execute a system command and return its status code (0 or 1).
+ // The system command's output (stderr or stdout) can be accessed from accessors.
+ public int execCommand() throws IOException, InterruptedException {
+ Process process = new ProcessBuilder(cmdOptions).start();
+
+ // you need this if you're going to write something to the command's input stream
+ // (such as when invoking the 'sudo' command, and it prompts you for a password).
+ OutputStream stdOutput = process.getOutputStream();
+
+ // i'm currently doing these on a separate line here in case i need to set them to null
+ // to get the threads to stop.
+ // see http://java.sun.com/j2se/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html
+ InputStream inputStream = process.getInputStream();
+ InputStream errorStream = process.getErrorStream();
+
+ // these need to run as java threads to get the standard output and error from the command.
+ // the inputstream handler gets a reference to our stdOutput in case we need to write
+ // something to it, such as with the sudo command
+ inputStreamHandler = new ThreadedStreamHandler(inputStream, stdOutput, sudoPassword);
+ errorStreamHandler = new ThreadedStreamHandler(errorStream);
+
+ // TODO the inputStreamHandler has a nasty side-effect of hanging if the given password is wrong; fix it.
+ inputStreamHandler.start();
+ errorStreamHandler.start();
+
+ int exitStatus = process.waitFor();
+
+ inputStreamHandler.interrupt();
+ errorStreamHandler.interrupt();
+ inputStreamHandler.join();
+ errorStreamHandler.join();
+ return exitStatus;
+ }
+
+
+ // Get the standard error from an executed system command.
+ public StringBuilder getStandardErrorFromCommand() {
+ return errorStreamHandler.getOutputBuffer();
+ }
+
+ // Get the standard output from an executed system command.
+ public StringBuilder getStandardOutputFromCommand() {
+ return inputStreamHandler.getOutputBuffer();
+ }
+}
diff --git a/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java
new file mode 100644
index 0000000000..7390b8807e
--- /dev/null
+++ b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java
@@ -0,0 +1,128 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.apitest.linux;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * This class is intended to be used with the SystemCommandExecutor
+ * class to let users execute system commands from Java applications.
+ *
+ * This class is based on work that was shared in a JavaWorld article
+ * named "When System.exec() won't". That article is available at this
+ * url:
+ *
+ * http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html
+ *
+ * Documentation for this class is available at this URL:
+ *
+ * http://devdaily.com/java/java-processbuilder-process-system-exec
+ *
+ *
+ * Copyright 2010 alvin j. alexander, devdaily.com.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser Public License for more details.
+ * You should have received a copy of the GNU Lesser Public License
+ * along with this program. If not, see .
+ *
+ * Please ee the following page for the LGPL license:
+ * http://www.gnu.org/licenses/lgpl.txt
+ *
+ */
+@Slf4j
+class ThreadedStreamHandler extends Thread {
+ final InputStream inputStream;
+ String adminPassword;
+ @SuppressWarnings("unused")
+ OutputStream outputStream;
+ PrintWriter printWriter;
+ final StringBuilder outputBuffer = new StringBuilder();
+ private boolean sudoIsRequested = false;
+
+ /**
+ * A simple constructor for when the sudo command is not necessary.
+ * This constructor will just run the command you provide, without
+ * running sudo before the command, and without expecting a password.
+ *
+ * @param inputStream InputStream
+ */
+ ThreadedStreamHandler(InputStream inputStream) {
+ this.inputStream = inputStream;
+ }
+
+ /**
+ * Use this constructor when you want to invoke the 'sudo' command.
+ * The outputStream must not be null. If it is, you'll regret it. :)
+ *
+ * TODO this currently hangs if the admin password given for the sudo command is wrong.
+ *
+ * @param inputStream InputStream
+ * @param outputStream OutputStream
+ * @param adminPassword String
+ */
+ ThreadedStreamHandler(InputStream inputStream, OutputStream outputStream, String adminPassword) {
+ this.inputStream = inputStream;
+ this.outputStream = outputStream;
+ this.printWriter = new PrintWriter(outputStream);
+ this.adminPassword = adminPassword;
+ this.sudoIsRequested = true;
+ }
+
+ public void run() {
+ // On mac os x 10.5.x, the admin password needs to be written immediately.
+ if (sudoIsRequested) {
+ printWriter.println(adminPassword);
+ printWriter.flush();
+ }
+
+ try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ while ((line = bufferedReader.readLine()) != null)
+ outputBuffer.append(line).append("\n");
+
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private void doSleep(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException ignored) {
+ }
+ }
+
+ public StringBuilder getOutputBuffer() {
+ return outputBuffer;
+ }
+}
+