mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 09:52:23 +01:00
Support starting bitcoin & bisq apps on Linux
The apitest.linux package is for running random bash commands, running 'bitcoind -regtest', running random 'bitcoin-cli -regtest' commands, and spinning up Bisq apps such as seednode, arbnode, and bob & alice nodes. All but random bash and bitcoin-cli commands are run in the background. The bitcoin-cli response processing is crude; a more sophiticated bitcoin-core rpc interface is not in the scope of this PR.
This commit is contained in:
parent
77b6878ec6
commit
c0c75e2471
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
149
apitest/src/main/java/bisq/apitest/linux/BashCommand.java
Normal file
149
apitest/src/main/java/bisq/apitest/linux/BashCommand.java
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String>
|
||||
public String getOutput() {
|
||||
return this.output;
|
||||
}
|
||||
|
||||
// TODO return Optional<String>
|
||||
public String getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<String> 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");
|
||||
}
|
||||
}
|
257
apitest/src/main/java/bisq/apitest/linux/BisqApp.java
Normal file
257
apitest/src/main/java/bisq/apitest/linux/BisqApp.java
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Throwable> 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<Throwable> 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<String> 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");
|
||||
}
|
||||
}
|
166
apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java
Normal file
166
apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
91
apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java
Normal file
91
apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
30
apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java
Normal file
30
apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Please ee the following page for the LGPL license:
|
||||
* http://www.gnu.org/licenses/lgpl.txt
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
class SystemCommandExecutor {
|
||||
private final List<String> 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<String> 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();
|
||||
}
|
||||
}
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user