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:
ghubstan 2020-07-09 15:35:45 -03:00
parent 77b6878ec6
commit c0c75e2471
No known key found for this signature in database
GPG Key ID: E35592D6800A861E
8 changed files with 1031 additions and 0 deletions

View 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.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");
}
}

View 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");
}
}

View 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");
}
}

View 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");
}
}

View 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");
}
}
}

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

View File

@ -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();
}
}

View File

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