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; + } +} +