This commit is contained in:
woodser 2021-05-04 20:20:01 -04:00
commit 8a38081c04
2800 changed files with 344130 additions and 0 deletions

View file

@ -0,0 +1,80 @@
/*
* 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;
import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.Scaffold.EXIT_FAILURE;
import static bisq.apitest.Scaffold.EXIT_SUCCESS;
import static java.lang.System.err;
import static java.lang.System.exit;
import bisq.apitest.config.ApiTestConfig;
/**
* ApiTestMain is a placeholder for the gradle build file, which requires a valid
* 'mainClassName' property in the :apitest subproject configuration.
*
* It does has some uses:
*
* It can be used to print test scaffolding options: bisq-apitest --help.
*
* It can be used to smoke test your bitcoind environment: bisq-apitest.
*
* It can be used to run the regtest/dao environment for release testing:
* bisq-test --shutdownAfterTests=false
*
* All method, scenario and end to end tests are found in the test sources folder.
*
* Requires bitcoind v0.19, v0.20, or v0.21.
*/
@Slf4j
public class ApiTestMain {
public static void main(String[] args) {
new ApiTestMain().execute(args);
}
public void execute(@SuppressWarnings("unused") String[] args) {
try {
Scaffold scaffold = new Scaffold(args).setUp();
ApiTestConfig config = scaffold.config;
if (config.skipTests) {
log.info("Skipping tests ...");
} else {
new SmokeTestBitcoind(config).run();
}
if (config.shutdownAfterTests) {
scaffold.tearDown();
exit(EXIT_SUCCESS);
} else {
log.info("Not shutting down scaffolding background processes will run until ^C / kill -15 is rcvd ...");
}
} catch (Throwable ex) {
err.println("Fault: An unexpected error occurred. " +
"Please file a report at https://bisq.network/issues");
ex.printStackTrace(err);
exit(EXIT_FAILURE);
}
}
}

View file

@ -0,0 +1,469 @@
/*
* 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;
import bisq.common.config.BisqHelpFormatter;
import bisq.common.util.Utilities;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermissions;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.ApiTestConfig.MEDIATOR;
import static bisq.apitest.config.ApiTestConfig.REFUND_AGENT;
import static bisq.apitest.config.BisqAppConfig.*;
import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY;
import static java.lang.String.format;
import static java.lang.System.exit;
import static java.lang.System.out;
import static java.net.InetAddress.getLoopbackAddress;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.apitest.config.ApiTestConfig;
import bisq.apitest.config.BisqAppConfig;
import bisq.apitest.linux.BashCommand;
import bisq.apitest.linux.BisqProcess;
import bisq.apitest.linux.BitcoinDaemon;
import bisq.apitest.linux.LinuxProcess;
import bisq.cli.GrpcClient;
@Slf4j
public class Scaffold {
public static final int EXIT_SUCCESS = 0;
public static final int EXIT_FAILURE = 1;
public enum BitcoinCoreApp {
bitcoind
}
public final ApiTestConfig config;
@Nullable
private SetupTask bitcoindTask;
@Nullable
private Future<SetupTask.Status> bitcoindTaskFuture;
@Nullable
private SetupTask seedNodeTask;
@Nullable
private Future<SetupTask.Status> seedNodeTaskFuture;
@Nullable
private SetupTask arbNodeTask;
@Nullable
private Future<SetupTask.Status> arbNodeTaskFuture;
@Nullable
private SetupTask aliceNodeTask;
@Nullable
private Future<SetupTask.Status> aliceNodeTaskFuture;
@Nullable
private SetupTask bobNodeTask;
@Nullable
private Future<SetupTask.Status> bobNodeTaskFuture;
private final ExecutorService executor;
/**
* Constructor for passing comma delimited list of supporting apps to
* ApiTestConfig, e.g., "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon".
*
* @param supportingApps String
*/
public Scaffold(String supportingApps) {
this(new ApiTestConfig("--supportingApps", supportingApps));
}
/**
* Constructor for passing options accepted by ApiTestConfig.
*
* @param args String[]
*/
public Scaffold(String[] args) {
this(new ApiTestConfig(args));
}
/**
* Constructor for passing ApiTestConfig instance.
*
* @param config ApiTestConfig
*/
public Scaffold(ApiTestConfig config) {
verifyNotWindows();
this.config = config;
this.executor = Executors.newFixedThreadPool(config.supportingApps.size());
if (config.helpRequested) {
config.printHelp(out,
new BisqHelpFormatter(
"Bisq ApiTest",
"bisq-apitest",
"0.1.0"));
exit(EXIT_SUCCESS);
}
}
public Scaffold setUp() throws IOException, InterruptedException, ExecutionException {
installDaoSetupDirectories();
// Start each background process from an executor, then add a shutdown hook.
CountDownLatch countdownLatch = new CountDownLatch(config.supportingApps.size());
startBackgroundProcesses(executor, countdownLatch);
installShutdownHook();
// Wait for all submitted startup tasks to decrement the count of the latch.
Objects.requireNonNull(countdownLatch).await();
// Verify each startup task's future is done.
verifyStartupCompleted();
maybeRegisterDisputeAgents();
return this;
}
public void tearDown() {
if (!executor.isTerminated()) {
try {
log.info("Shutting down executor service ...");
executor.shutdownNow();
//noinspection ResultOfMethodCallIgnored
executor.awaitTermination(config.supportingApps.size() * 2000L, MILLISECONDS);
SetupTask[] orderedTasks = new SetupTask[]{
bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask};
Optional<Throwable> firstException = shutDownAll(orderedTasks);
if (firstException.isPresent())
throw new IllegalStateException(
"There were errors shutting down one or more background instances.",
firstException.get());
else
log.info("Teardown complete");
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
private Optional<Throwable> shutDownAll(SetupTask[] orderedTasks) {
Optional<Throwable> firstException = Optional.empty();
for (SetupTask t : orderedTasks) {
if (t != null && t.getLinuxProcess() != null) {
try {
LinuxProcess p = t.getLinuxProcess();
p.shutdown();
MILLISECONDS.sleep(1000);
if (p.hasShutdownExceptions()) {
// We log shutdown exceptions, but do not throw any from here
// because all of the background instances must be shut down.
p.logExceptions(p.getShutdownExceptions(), log);
// We cache only the 1st shutdown exception and move on to the
// next process to be shutdown. This cached exception will be the
// one thrown to the calling test case (the @AfterAll method).
if (!firstException.isPresent())
firstException = Optional.of(p.getShutdownExceptions().get(0));
}
} catch (InterruptedException ignored) {
// empty
}
}
}
return firstException;
}
public void installDaoSetupDirectories() {
cleanDaoSetupDirectories();
String daoSetupDir = Paths.get(config.baseSrcResourcesDir, "dao-setup").toFile().getAbsolutePath();
String buildDataDir = config.rootAppDataDir.getAbsolutePath();
try {
if (!new File(daoSetupDir).exists())
throw new FileNotFoundException(
format("Dao setup dir '%s' not found. Run gradle :apitest:installDaoSetup"
+ " to download dao-setup.zip and extract contents to resources folder",
daoSetupDir));
BashCommand copyBitcoinRegtestDir = new BashCommand(
"cp -rf " + daoSetupDir + "/Bitcoin-regtest/regtest"
+ " " + config.bitcoinDatadir);
if (copyBitcoinRegtestDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not install bitcoin regtest dir");
String aliceDataDir = daoSetupDir + "/" + alicedaemon.appName;
BashCommand copyAliceDataDir = new BashCommand(
"cp -rf " + aliceDataDir + " " + config.rootAppDataDir);
if (copyAliceDataDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not install alice data dir");
String bobDataDir = daoSetupDir + "/" + bobdaemon.appName;
BashCommand copyBobDataDir = new BashCommand(
"cp -rf " + bobDataDir + " " + config.rootAppDataDir);
if (copyBobDataDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not install bob data dir");
log.info("Installed dao-setup files into {}", buildDataDir);
if (!config.callRateMeteringConfigPath.isEmpty()) {
installCallRateMeteringConfiguration(aliceDataDir);
installCallRateMeteringConfiguration(bobDataDir);
}
// Copy the blocknotify script from the src resources dir to the build
// resources dir. Users may want to edit comment out some lines when all
// of the default block notifcation ports being will not be used (to avoid
// seeing rpc notifcation warnings in log files).
installBitcoinBlocknotify();
} catch (IOException | InterruptedException ex) {
throw new IllegalStateException("Could not install dao-setup files from " + daoSetupDir, ex);
}
}
private void cleanDaoSetupDirectories() {
String buildDataDir = config.rootAppDataDir.getAbsolutePath();
log.info("Cleaning dao-setup data in {}", buildDataDir);
try {
BashCommand rmBobDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + bobdaemon.appName);
if (rmBobDataDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not delete bob data dir");
BashCommand rmAliceDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + alicedaemon.appName);
if (rmAliceDataDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not delete alice data dir");
BashCommand rmArbNodeDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + arbdaemon.appName);
if (rmArbNodeDataDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not delete arbitrator data dir");
BashCommand rmSeedNodeDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + seednode.appName);
if (rmSeedNodeDataDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not delete seednode data dir");
BashCommand rmBitcoinRegtestDir = new BashCommand("rm -rf " + config.bitcoinDatadir + "/regtest");
if (rmBitcoinRegtestDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not clean bitcoind regtest dir");
} catch (IOException | InterruptedException ex) {
throw new IllegalStateException("Could not clean dao-setup files from " + buildDataDir, ex);
}
}
private void installBitcoinBlocknotify() {
// gradle is not working for this
try {
Path srcPath = Paths.get(config.baseSrcResourcesDir, "blocknotify");
Path destPath = Paths.get(config.bitcoinDatadir, "blocknotify");
Files.copy(srcPath, destPath, REPLACE_EXISTING);
String chmod700Perms = "rwx------";
Files.setPosixFilePermissions(destPath, PosixFilePermissions.fromString(chmod700Perms));
log.info("Installed {} with perms {}.", destPath, chmod700Perms);
} catch (IOException e) {
e.printStackTrace();
}
}
private void installCallRateMeteringConfiguration(String dataDir) throws IOException, InterruptedException {
File testRateMeteringFile = new File(config.callRateMeteringConfigPath);
if (!testRateMeteringFile.exists())
throw new FileNotFoundException(
format("Call rate metering config file '%s' not found", config.callRateMeteringConfigPath));
BashCommand copyRateMeteringConfigFile = new BashCommand(
"cp -rf " + config.callRateMeteringConfigPath + " " + dataDir);
if (copyRateMeteringConfigFile.run().getExitStatus() != 0)
throw new IllegalStateException(
format("Could not install %s file in %s",
testRateMeteringFile.getAbsolutePath(), dataDir));
Path destPath = Paths.get(dataDir, testRateMeteringFile.getName());
String chmod700Perms = "rwx------";
Files.setPosixFilePermissions(destPath, PosixFilePermissions.fromString(chmod700Perms));
log.info("Installed {} with perms {}.", destPath, chmod700Perms);
}
private void installShutdownHook() {
// Background apps can be left running until the jvm is manually shutdown,
// so we add a shutdown hook for that use case.
Runtime.getRuntime().addShutdownHook(new Thread(this::tearDown));
}
// Starts bitcoind and bisq apps (seednode, arbnode, etc...)
private void startBackgroundProcesses(ExecutorService executor,
CountDownLatch countdownLatch)
throws InterruptedException, IOException {
log.info("Starting supporting apps {}", config.supportingApps.toString());
if (config.hasSupportingApp(bitcoind.name())) {
BitcoinDaemon bitcoinDaemon = new BitcoinDaemon(config);
bitcoinDaemon.verifyBitcoinPathsExist(true);
bitcoindTask = new SetupTask(bitcoinDaemon, countdownLatch);
bitcoindTaskFuture = executor.submit(bitcoindTask);
MILLISECONDS.sleep(config.bisqAppInitTime);
LinuxProcess bitcoindProcess = bitcoindTask.getLinuxProcess();
if (bitcoindProcess.hasStartupExceptions()) {
bitcoindProcess.logExceptions(bitcoindProcess.getStartupExceptions(), log);
throw new IllegalStateException(bitcoindProcess.getStartupExceptions().get(0));
}
bitcoinDaemon.verifyBitcoindRunning();
}
// Start Bisq apps defined by the supportingApps option, in the in proper order.
if (config.hasSupportingApp(seednode.name()))
startBisqApp(seednode, executor, countdownLatch);
if (config.hasSupportingApp(arbdaemon.name()))
startBisqApp(arbdaemon, executor, countdownLatch);
else if (config.hasSupportingApp(arbdesktop.name()))
startBisqApp(arbdesktop, executor, countdownLatch);
if (config.hasSupportingApp(alicedaemon.name()))
startBisqApp(alicedaemon, executor, countdownLatch);
else if (config.hasSupportingApp(alicedesktop.name()))
startBisqApp(alicedesktop, executor, countdownLatch);
if (config.hasSupportingApp(bobdaemon.name()))
startBisqApp(bobdaemon, executor, countdownLatch);
else if (config.hasSupportingApp(bobdesktop.name()))
startBisqApp(bobdesktop, executor, countdownLatch);
}
private void startBisqApp(BisqAppConfig bisqAppConfig,
ExecutorService executor,
CountDownLatch countdownLatch)
throws IOException, InterruptedException {
BisqProcess bisqProcess = createBisqProcess(bisqAppConfig);
switch (bisqAppConfig) {
case seednode:
seedNodeTask = new SetupTask(bisqProcess, countdownLatch);
seedNodeTaskFuture = executor.submit(seedNodeTask);
break;
case arbdaemon:
case arbdesktop:
arbNodeTask = new SetupTask(bisqProcess, countdownLatch);
arbNodeTaskFuture = executor.submit(arbNodeTask);
break;
case alicedaemon:
case alicedesktop:
aliceNodeTask = new SetupTask(bisqProcess, countdownLatch);
aliceNodeTaskFuture = executor.submit(aliceNodeTask);
break;
case bobdaemon:
case bobdesktop:
bobNodeTask = new SetupTask(bisqProcess, countdownLatch);
bobNodeTaskFuture = executor.submit(bobNodeTask);
break;
default:
throw new IllegalStateException("Unknown BisqAppConfig " + bisqAppConfig.name());
}
log.info("Giving {} ms for {} to initialize ...", config.bisqAppInitTime, bisqAppConfig.appName);
MILLISECONDS.sleep(config.bisqAppInitTime);
if (bisqProcess.hasStartupExceptions()) {
bisqProcess.logExceptions(bisqProcess.getStartupExceptions(), log);
throw new IllegalStateException(bisqProcess.getStartupExceptions().get(0));
}
}
private BisqProcess createBisqProcess(BisqAppConfig bisqAppConfig)
throws IOException, InterruptedException {
BisqProcess bisqProcess = new BisqProcess(bisqAppConfig, config);
bisqProcess.verifyAppNotRunning();
bisqProcess.verifyAppDataDirInstalled();
return bisqProcess;
}
private void verifyStartupCompleted()
throws ExecutionException, InterruptedException {
if (bitcoindTaskFuture != null)
verifyStartupCompleted(bitcoindTaskFuture);
if (seedNodeTaskFuture != null)
verifyStartupCompleted(seedNodeTaskFuture);
if (arbNodeTaskFuture != null)
verifyStartupCompleted(arbNodeTaskFuture);
if (aliceNodeTaskFuture != null)
verifyStartupCompleted(aliceNodeTaskFuture);
if (bobNodeTaskFuture != null)
verifyStartupCompleted(bobNodeTaskFuture);
}
private void verifyStartupCompleted(Future<SetupTask.Status> futureStatus)
throws ExecutionException, InterruptedException {
for (int i = 0; i < 10; i++) {
if (futureStatus.isDone()) {
log.info("{} completed startup at {} {}",
futureStatus.get().getName(),
futureStatus.get().getStartTime().toLocalDate(),
futureStatus.get().getStartTime().toLocalTime());
return;
} else {
// We are giving the thread more time to terminate after the countdown
// latch reached 0. If we are running only bitcoind, we need to be even
// more lenient.
SECONDS.sleep(config.supportingApps.size() == 1 ? 2 : 1);
}
}
throw new IllegalStateException(format("%s did not complete startup", futureStatus.get().getName()));
}
private void verifyNotWindows() {
if (Utilities.isWindows())
throw new IllegalStateException("ApiTest not supported on Windows");
}
private void maybeRegisterDisputeAgents() {
if (config.hasSupportingApp(arbdaemon.name()) && config.registerDisputeAgents) {
log.info("Option --registerDisputeAgents=true, registering dispute agents in arbdaemon ...");
GrpcClient arbClient = new GrpcClient(getLoopbackAddress().getHostAddress(),
arbdaemon.apiPort,
config.apiPassword);
arbClient.registerDisputeAgent(MEDIATOR, DEV_PRIVILEGE_PRIV_KEY);
arbClient.registerDisputeAgent(REFUND_AGENT, DEV_PRIVILEGE_PRIV_KEY);
}
}
}

View file

@ -0,0 +1,85 @@
/*
* 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;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import lombok.extern.slf4j.Slf4j;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import bisq.apitest.linux.LinuxProcess;
@Slf4j
public class SetupTask implements Callable<SetupTask.Status> {
private final LinuxProcess linuxProcess;
private final CountDownLatch countdownLatch;
public SetupTask(LinuxProcess linuxProcess, CountDownLatch countdownLatch) {
this.linuxProcess = linuxProcess;
this.countdownLatch = countdownLatch;
}
@Override
public Status call() throws Exception {
try {
linuxProcess.start(); // always runs in background
MILLISECONDS.sleep(1000); // give 1s for bg process to init
} catch (InterruptedException ex) {
throw new IllegalStateException(format("Error starting %s", linuxProcess.getName()), ex);
}
Objects.requireNonNull(countdownLatch).countDown();
return new Status(linuxProcess.getName(), LocalDateTime.now());
}
public LinuxProcess getLinuxProcess() {
return linuxProcess;
}
public static class Status {
private final String name;
private final LocalDateTime startTime;
public Status(String name, LocalDateTime startTime) {
super();
this.name = name;
this.startTime = startTime;
}
public String getName() {
return name;
}
public LocalDateTime getStartTime() {
return startTime;
}
@Override
public String toString() {
return "SetupTask.Status [name=" + name + ", completionTime=" + startTime + "]";
}
}
}

View file

@ -0,0 +1,51 @@
/*
* 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;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import bisq.apitest.linux.BashCommand;
@Slf4j
class SmokeTestBashCommand {
public SmokeTestBashCommand() {
}
public void runSmokeTest() {
try {
BashCommand cmd = new BashCommand("ls -l").run();
log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput());
cmd = new BashCommand("free -g").run();
log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput());
cmd = new BashCommand("date").run();
log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput());
cmd = new BashCommand("netstat -a | grep localhost").run();
log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,72 @@
/*
* 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;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import static java.lang.String.format;
import bisq.apitest.config.ApiTestConfig;
import bisq.apitest.linux.BitcoinCli;
@Slf4j
class SmokeTestBitcoind {
private final ApiTestConfig config;
public SmokeTestBitcoind(ApiTestConfig config) {
this.config = config;
}
public void run() throws IOException, InterruptedException {
runBitcoinGetWalletInfo(); // smoke test bitcoin-cli
String newBitcoinAddress = getNewAddress();
generateToAddress(1, newBitcoinAddress);
}
public void runBitcoinGetWalletInfo() throws IOException, InterruptedException {
// This might be good for a sanity check to make sure the regtest data was installed.
log.info("Smoke test bitcoin-cli getwalletinfo");
BitcoinCli walletInfo = new BitcoinCli(config, "getwalletinfo").run();
log.info("{}\n{}", walletInfo.getCommandWithOptions(), walletInfo.getOutput());
log.info("balance str = {}", walletInfo.getOutputValueAsString("balance"));
log.info("balance dbl = {}", walletInfo.getOutputValueAsDouble("balance"));
log.info("keypoololdest long = {}", walletInfo.getOutputValueAsLong("keypoololdest"));
log.info("paytxfee dbl = {}", walletInfo.getOutputValueAsDouble("paytxfee"));
log.info("keypoolsize_hd_internal int = {}", walletInfo.getOutputValueAsInt("keypoolsize_hd_internal"));
log.info("private_keys_enabled bool = {}", walletInfo.getOutputValueAsBoolean("private_keys_enabled"));
log.info("hdseedid str = {}", walletInfo.getOutputValueAsString("hdseedid"));
}
public String getNewAddress() throws IOException, InterruptedException {
BitcoinCli newAddress = new BitcoinCli(config, "getnewaddress").run();
log.info("{}\n{}", newAddress.getCommandWithOptions(), newAddress.getOutput());
return newAddress.getOutput();
}
public void generateToAddress(int blocks, String address) throws IOException, InterruptedException {
String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address);
BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run();
// Return value is an array of TxIDs.
log.info("{}\n{}", generateToAddress.getCommandWithOptions(), generateToAddress.getOutputValueAsStringArray());
}
}

View file

@ -0,0 +1,380 @@
/*
* 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.config;
import bisq.common.config.CompositeOptionSet;
import joptsimple.AbstractOptionSpec;
import joptsimple.ArgumentAcceptingOptionSpec;
import joptsimple.HelpFormatter;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import java.net.InetAddress;
import java.nio.file.Paths;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.lang.System.getenv;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static joptsimple.internal.Strings.EMPTY;
@Slf4j
public class ApiTestConfig {
// Global constants
public static final String BSQ = "BSQ";
public static final String BTC = "BTC";
public static final String ARBITRATOR = "arbitrator";
public static final String MEDIATOR = "mediator";
public static final String REFUND_AGENT = "refundagent";
// Option name constants
static final String HELP = "help";
static final String BASH_PATH = "bashPath";
static final String BERKELEYDB_LIB_PATH = "berkeleyDbLibPath";
static final String BITCOIN_PATH = "bitcoinPath";
static final String BITCOIN_RPC_PORT = "bitcoinRpcPort";
static final String BITCOIN_RPC_USER = "bitcoinRpcUser";
static final String BITCOIN_RPC_PASSWORD = "bitcoinRpcPassword";
static final String BITCOIN_REGTEST_HOST = "bitcoinRegtestHost";
static final String CONFIG_FILE = "configFile";
static final String ROOT_APP_DATA_DIR = "rootAppDataDir";
static final String API_PASSWORD = "apiPassword";
static final String RUN_SUBPROJECT_JARS = "runSubprojectJars";
static final String BISQ_APP_INIT_TIME = "bisqAppInitTime";
static final String SKIP_TESTS = "skipTests";
static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests";
static final String SUPPORTING_APPS = "supportingApps";
static final String CALL_RATE_METERING_CONFIG_PATH = "callRateMeteringConfigPath";
static final String ENABLE_BISQ_DEBUGGING = "enableBisqDebugging";
static final String REGISTER_DISPUTE_AGENTS = "registerDisputeAgents";
// Default values for certain options
static final String DEFAULT_CONFIG_FILE_NAME = "apitest.properties";
// Static fields that provide access to Config properties in locations where injecting
// a Config instance is not feasible.
public static String BASH_PATH_VALUE;
public final File defaultConfigFile;
// Options supported only at the command line, not within a config file.
public final boolean helpRequested;
public final File configFile;
// Options supported at the command line and a config file.
public final File rootAppDataDir;
public final String bashPath;
public final String berkeleyDbLibPath;
public final String bitcoinPath;
public final String bitcoinRegtestHost;
public final int bitcoinRpcPort;
public final String bitcoinRpcUser;
public final String bitcoinRpcPassword;
// Daemon instances can use same gRPC password, but each needs a different apiPort.
public final String apiPassword;
public final boolean runSubprojectJars;
public final long bisqAppInitTime;
public final boolean skipTests;
public final boolean shutdownAfterTests;
public final List<String> supportingApps;
public final String callRateMeteringConfigPath;
public final boolean enableBisqDebugging;
public final boolean registerDisputeAgents;
// Immutable system configurations set in the constructor.
public final String bitcoinDatadir;
public final String userDir;
public final boolean isRunningTest;
public final String rootProjectDir;
public final String baseBuildResourcesDir;
public final String baseSrcResourcesDir;
// The parser that will be used to parse both cmd line and config file options
private final OptionParser parser = new OptionParser();
public ApiTestConfig(String... args) {
this.userDir = getProperty("user.dir");
// If running a @Test, the current working directory is the :apitest subproject
// folder. If running ApiTestMain, the current working directory is the
// bisq root project folder.
this.isRunningTest = Paths.get(userDir).getFileName().toString().equals("apitest");
this.rootProjectDir = isRunningTest
? Paths.get(userDir).getParent().toFile().getAbsolutePath()
: Paths.get(userDir).toFile().getAbsolutePath();
this.baseBuildResourcesDir = Paths.get(rootProjectDir, "apitest", "build", "resources", "main")
.toFile().getAbsolutePath();
this.baseSrcResourcesDir = Paths.get(rootProjectDir, "apitest", "src", "main", "resources")
.toFile().getAbsolutePath();
this.defaultConfigFile = absoluteConfigFile(baseBuildResourcesDir, DEFAULT_CONFIG_FILE_NAME);
this.bitcoinDatadir = Paths.get(baseBuildResourcesDir, "Bitcoin-regtest").toFile().getAbsolutePath();
AbstractOptionSpec<Void> helpOpt =
parser.accepts(HELP, "Print this help text")
.forHelp();
ArgumentAcceptingOptionSpec<String> configFileOpt =
parser.accepts(CONFIG_FILE, format("Specify configuration file. " +
"Relative paths will be prefixed by %s location.", userDir))
.withRequiredArg()
.ofType(String.class)
.defaultsTo(DEFAULT_CONFIG_FILE_NAME);
ArgumentAcceptingOptionSpec<File> appDataDirOpt =
parser.accepts(ROOT_APP_DATA_DIR, "Application data directory")
.withRequiredArg()
.ofType(File.class)
.defaultsTo(new File(baseBuildResourcesDir));
ArgumentAcceptingOptionSpec<String> bashPathOpt =
parser.accepts(BASH_PATH, "Bash path")
.withRequiredArg()
.ofType(String.class)
.defaultsTo(
(getenv("SHELL") == null || !getenv("SHELL").contains("bash"))
? "/bin/bash"
: getenv("SHELL"));
ArgumentAcceptingOptionSpec<String> berkeleyDbLibPathOpt =
parser.accepts(BERKELEYDB_LIB_PATH, "Berkeley DB lib path")
.withRequiredArg()
.ofType(String.class).defaultsTo(EMPTY);
ArgumentAcceptingOptionSpec<String> bitcoinPathOpt =
parser.accepts(BITCOIN_PATH, "Bitcoin path")
.withRequiredArg()
.ofType(String.class).defaultsTo("/usr/local/bin");
ArgumentAcceptingOptionSpec<String> bitcoinRegtestHostOpt =
parser.accepts(BITCOIN_REGTEST_HOST, "Bitcoin Core regtest host")
.withRequiredArg()
.ofType(String.class).defaultsTo(InetAddress.getLoopbackAddress().getHostAddress());
ArgumentAcceptingOptionSpec<Integer> bitcoinRpcPortOpt =
parser.accepts(BITCOIN_RPC_PORT, "Bitcoin Core rpc port (non-default)")
.withRequiredArg()
.ofType(Integer.class).defaultsTo(19443);
ArgumentAcceptingOptionSpec<String> bitcoinRpcUserOpt =
parser.accepts(BITCOIN_RPC_USER, "Bitcoin rpc user")
.withRequiredArg()
.ofType(String.class).defaultsTo("apitest");
ArgumentAcceptingOptionSpec<String> bitcoinRpcPasswordOpt =
parser.accepts(BITCOIN_RPC_PASSWORD, "Bitcoin rpc password")
.withRequiredArg()
.ofType(String.class).defaultsTo("apitest");
ArgumentAcceptingOptionSpec<String> apiPasswordOpt =
parser.accepts(API_PASSWORD, "gRPC API password")
.withRequiredArg()
.defaultsTo("xyz");
ArgumentAcceptingOptionSpec<Boolean> runSubprojectJarsOpt =
parser.accepts(RUN_SUBPROJECT_JARS,
"Run subproject build jars instead of full build jars")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Long> bisqAppInitTimeOpt =
parser.accepts(BISQ_APP_INIT_TIME,
"Amount of time (ms) to wait on a Bisq instance's initialization")
.withRequiredArg()
.ofType(Long.class)
.defaultsTo(5000L);
ArgumentAcceptingOptionSpec<Boolean> skipTestsOpt =
parser.accepts(SKIP_TESTS,
"Start apps, but skip tests")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> shutdownAfterTestsOpt =
parser.accepts(SHUTDOWN_AFTER_TESTS,
"Terminate all processes after tests")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(true);
ArgumentAcceptingOptionSpec<String> supportingAppsOpt =
parser.accepts(SUPPORTING_APPS,
"Comma delimited list of supporting apps (bitcoind,seednode,arbdaemon,...")
.withRequiredArg()
.ofType(String.class)
.defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon");
ArgumentAcceptingOptionSpec<String> callRateMeteringConfigPathOpt =
parser.accepts(CALL_RATE_METERING_CONFIG_PATH,
"Install a ratemeters.json file to configure call rate metering interceptors")
.withRequiredArg()
.defaultsTo(EMPTY);
ArgumentAcceptingOptionSpec<Boolean> enableBisqDebuggingOpt =
parser.accepts(ENABLE_BISQ_DEBUGGING,
"Start Bisq apps with remote debug options")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> registerDisputeAgentsOpt =
parser.accepts(REGISTER_DISPUTE_AGENTS,
"Register dispute agents in arbitration daemon")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(true);
try {
CompositeOptionSet options = new CompositeOptionSet();
// Parse command line options
OptionSet cliOpts = parser.parse(args);
options.addOptionSet(cliOpts);
// Parse config file specified at the command line only if it was specified as
// an absolute path. Otherwise, the config file will be processed later below.
File configFile = null;
OptionSpec<?>[] disallowedOpts = new OptionSpec<?>[]{helpOpt, configFileOpt};
final boolean cliHasConfigFileOpt = cliOpts.has(configFileOpt);
boolean configFileHasBeenProcessed = false;
if (cliHasConfigFileOpt) {
configFile = new File(cliOpts.valueOf(configFileOpt));
if (configFile.isAbsolute()) {
Optional<OptionSet> configFileOpts = parseOptionsFrom(configFile, disallowedOpts);
if (configFileOpts.isPresent()) {
options.addOptionSet(configFileOpts.get());
configFileHasBeenProcessed = true;
}
}
}
// If the config file has not yet been processed, either because a relative
// path was provided at the command line, or because no value was provided at
// the command line, attempt to process the file now, falling back to the
// default config file location if none was specified at the command line.
if (!configFileHasBeenProcessed) {
configFile = cliHasConfigFileOpt && !configFile.isAbsolute() ?
absoluteConfigFile(userDir, configFile.getPath()) :
defaultConfigFile;
Optional<OptionSet> configFileOpts = parseOptionsFrom(configFile, disallowedOpts);
configFileOpts.ifPresent(options::addOptionSet);
}
// Assign all remaining properties, with command line options taking
// precedence over those provided in the config file (if any)
this.helpRequested = options.has(helpOpt);
this.configFile = configFile;
this.rootAppDataDir = options.valueOf(appDataDirOpt);
bashPath = options.valueOf(bashPathOpt);
this.berkeleyDbLibPath = options.valueOf(berkeleyDbLibPathOpt);
this.bitcoinPath = options.valueOf(bitcoinPathOpt);
this.bitcoinRegtestHost = options.valueOf(bitcoinRegtestHostOpt);
this.bitcoinRpcPort = options.valueOf(bitcoinRpcPortOpt);
this.bitcoinRpcUser = options.valueOf(bitcoinRpcUserOpt);
this.bitcoinRpcPassword = options.valueOf(bitcoinRpcPasswordOpt);
this.apiPassword = options.valueOf(apiPasswordOpt);
this.runSubprojectJars = options.valueOf(runSubprojectJarsOpt);
this.bisqAppInitTime = options.valueOf(bisqAppInitTimeOpt);
this.skipTests = options.valueOf(skipTestsOpt);
this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt);
this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(","));
this.callRateMeteringConfigPath = options.valueOf(callRateMeteringConfigPathOpt);
this.enableBisqDebugging = options.valueOf(enableBisqDebuggingOpt);
this.registerDisputeAgents = options.valueOf(registerDisputeAgentsOpt);
// Assign values to special-case static fields.
BASH_PATH_VALUE = bashPath;
} catch (OptionException ex) {
throw new IllegalStateException(format("Problem parsing option '%s': %s",
ex.options().get(0),
ex.getCause() != null ?
ex.getCause().getMessage() :
ex.getMessage()));
}
}
public boolean hasSupportingApp(String... supportingApp) {
return stream(supportingApp).anyMatch(this.supportingApps::contains);
}
public void printHelp(OutputStream sink, HelpFormatter formatter) {
try {
parser.formatHelpWith(formatter);
parser.printHelpOn(sink);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private Optional<OptionSet> parseOptionsFrom(File configFile, OptionSpec<?>[] disallowedOpts) {
if (!configFile.exists() && !configFile.equals(absoluteConfigFile(userDir, DEFAULT_CONFIG_FILE_NAME)))
throw new IllegalStateException(format("The specified config file '%s' does not exist.", configFile));
Properties properties = getProperties(configFile);
List<String> optionLines = new ArrayList<>();
properties.forEach((k, v) -> {
optionLines.add("--" + k + "=" + v); // dashes expected by jopt parser below
});
OptionSet configFileOpts = parser.parse(optionLines.toArray(new String[0]));
for (OptionSpec<?> disallowedOpt : disallowedOpts)
if (configFileOpts.has(disallowedOpt))
throw new IllegalStateException(
format("The '%s' option is disallowed in config files",
disallowedOpt.options().get(0)));
return Optional.of(configFileOpts);
}
private Properties getProperties(File configFile) {
try {
Properties properties = new Properties();
properties.load(new FileInputStream(configFile.getAbsolutePath()));
return properties;
} catch (IOException ex) {
throw new IllegalStateException(
format("Could not load properties from config file %s",
configFile.getAbsolutePath()), ex);
}
}
private static File absoluteConfigFile(String parentDir, String relativeConfigFilePath) {
return new File(parentDir, relativeConfigFilePath);
}
}

View file

@ -0,0 +1,133 @@
/*
* 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.config;
import bisq.seednode.SeedNodeMain;
import bisq.desktop.app.BisqAppMain;
import bisq.daemon.app.BisqDaemonMain;
/**
Some non user configurable Bisq seednode, arb node, bob and alice daemon option values.
@see <a href="https://github.com/bisq-network/bisq/blob/master/docs/dev-setup.md">dev-setup.md</a>
@see <a href="https://github.com/bisq-network/bisq/blob/master/docs/dao-setup.md">dao-setup.md</a>
*/
public enum BisqAppConfig {
seednode("bisq-BTC_REGTEST_Seed_2002",
"bisq-seednode",
"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml",
SeedNodeMain.class.getName(),
2002,
5120,
-1,
49996),
arbdaemon("bisq-BTC_REGTEST_Arb_dao",
"bisq-daemon",
"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml",
BisqDaemonMain.class.getName(),
4444,
5121,
9997,
49997),
arbdesktop("bisq-BTC_REGTEST_Arb_dao",
"bisq-desktop",
"-XX:MaxRAM=3g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml",
BisqAppMain.class.getName(),
4444,
5121,
-1,
49997),
alicedaemon("bisq-BTC_REGTEST_Alice_dao",
"bisq-daemon",
"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml",
BisqDaemonMain.class.getName(),
7777,
5122,
9998,
49998),
alicedesktop("bisq-BTC_REGTEST_Alice_dao",
"bisq-desktop",
"-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml",
BisqAppMain.class.getName(),
7777,
5122,
-1,
49998),
bobdaemon("bisq-BTC_REGTEST_Bob_dao",
"bisq-daemon",
"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml",
BisqDaemonMain.class.getName(),
8888,
5123,
9999,
49999),
bobdesktop("bisq-BTC_REGTEST_Bob_dao",
"bisq-desktop",
"-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml",
BisqAppMain.class.getName(),
8888,
5123,
-1,
49999);
public final String appName;
public final String startupScript;
public final String javaOpts;
public final String mainClassName;
public final int nodePort;
public final int rpcBlockNotificationPort;
// Daemons can use a global gRPC password, but each needs a unique apiPort.
public final int apiPort;
public final int remoteDebugPort;
BisqAppConfig(String appName,
String startupScript,
String javaOpts,
String mainClassName,
int nodePort,
int rpcBlockNotificationPort,
int apiPort,
int remoteDebugPort) {
this.appName = appName;
this.startupScript = startupScript;
this.javaOpts = javaOpts;
this.mainClassName = mainClassName;
this.nodePort = nodePort;
this.rpcBlockNotificationPort = rpcBlockNotificationPort;
this.apiPort = apiPort;
this.remoteDebugPort = remoteDebugPort;
}
@Override
public String toString() {
return "BisqAppConfig{" + "\n" +
" appName='" + appName + '\'' + "\n" +
", startupScript='" + startupScript + '\'' + "\n" +
", javaOpts='" + javaOpts + '\'' + "\n" +
", mainClassName='" + mainClassName + '\'' + "\n" +
", nodePort=" + nodePort + "\n" +
", rpcBlockNotificationPort=" + rpcBlockNotificationPort + "\n" +
", apiPort=" + apiPort + "\n" +
", remoteDebugPort=" + remoteDebugPort + "\n" +
'}';
}
}

View file

@ -0,0 +1,129 @@
/*
* 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 joptsimple.internal.Strings.EMPTY;
import bisq.apitest.config.ApiTestConfig;
@Slf4j
abstract class AbstractLinuxProcess implements LinuxProcess {
protected final String name;
protected final ApiTestConfig config;
protected long pid;
protected final List<Throwable> startupExceptions;
protected final List<Throwable> shutdownExceptions;
public AbstractLinuxProcess(String name, ApiTestConfig config) {
this.name = name;
this.config = config;
this.startupExceptions = new ArrayList<>();
this.shutdownExceptions = new ArrayList<>();
}
@Override
public String getName() {
return this.name;
}
@Override
public boolean hasStartupExceptions() {
return !startupExceptions.isEmpty();
}
@Override
public boolean hasShutdownExceptions() {
return !shutdownExceptions.isEmpty();
}
@Override
public void logExceptions(List<Throwable> exceptions, org.slf4j.Logger log) {
for (Throwable t : exceptions) {
log.error("", t);
}
}
@Override
public List<Throwable> getStartupExceptions() {
return startupExceptions;
}
@Override
public List<Throwable> getShutdownExceptions() {
return shutdownExceptions;
}
@SuppressWarnings("unused")
public void verifyBitcoinPathsExist() {
verifyBitcoinPathsExist(false);
}
public void verifyBitcoinPathsExist(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",
"berkeleyDbLibPath", config.berkeleyDbLibPath,
"bitcoinPath", config.bitcoinPath,
"bitcoinDatadir", config.bitcoinDatadir,
"blocknotify", config.bitcoinDatadir + "/blocknotify"));
if (!config.berkeleyDbLibPath.equals(EMPTY)) {
File berkeleyDbLibPath = new File(config.berkeleyDbLibPath);
if (!berkeleyDbLibPath.exists() || !berkeleyDbLibPath.canExecute())
throw new IllegalStateException(berkeleyDbLibPath + " cannot be found or executed");
}
File bitcoindExecutable = Paths.get(config.bitcoinPath, "bitcoind").toFile();
if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute())
throw new IllegalStateException(format("'%s' cannot be found or executed.%n"
+ "A bitcoin-core v0.19, v0.20, or v0.21 installation is required," +
" and the 'bitcoinPath' must be configured in 'apitest.properties'",
bitcoindExecutable.getAbsolutePath()));
File bitcoindDatadir = new File(config.bitcoinDatadir);
if (!bitcoindDatadir.exists() || !bitcoindDatadir.canWrite())
throw new IllegalStateException(bitcoindDatadir + " cannot be found or written to");
File blocknotify = new File(bitcoindDatadir, "blocknotify");
if (!blocknotify.exists() || !blocknotify.canExecute())
throw new IllegalStateException(blocknotify.getAbsolutePath() + " 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,156 @@
/*
* 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 bisq.apitest.config.ApiTestConfig.BASH_PATH_VALUE;
import static java.lang.management.ManagementFactory.getRuntimeMXBean;
@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.exec();
processOutput(commandExecutor);
return this;
}
public BashCommand runInBackground() throws IOException, InterruptedException {
SystemCommandExecutor commandExecutor = new SystemCommandExecutor(tokenizeSystemCommand());
exitStatus = commandExecutor.exec(false);
processOutput(commandExecutor);
return this;
}
private void processOutput(SystemCommandExecutor commandExecutor) {
// Get the error status and stderr from system command.
StringBuilder stderr = commandExecutor.getStandardErrorFromCommand();
if (stderr.length() > 0)
error = stderr.toString();
if (exitStatus != 0)
return;
// 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();
}
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(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,266 @@
/*
* 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 BisqProcess 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 String debugOpts;
public BisqProcess(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.isRunningTest ? "." : "./apitest")
+ "/scripts/get-bisq-pid.sh";
this.debugOpts = config.enableBisqDebugging
? " -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:" + bisqAppConfig.remoteDebugPort
: "";
}
@Override
public void start() {
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() {
try {
log.info("Shutting down {} ...", bisqAppConfig.appName);
if (!isAlive(pid)) {
this.shutdownExceptions.add(new IllegalStateException(format("%s already shut down", bisqAppConfig.appName)));
return;
}
String killCmd = "kill -15 " + pid;
if (new BashCommand(killCmd).run().getExitStatus() != 0) {
this.shutdownExceptions.add(new IllegalStateException(format("Could not shut down %s", bisqAppConfig.appName)));
return;
}
// Be lenient about the time it takes for a java app to shut down.
for (int i = 0; i < 5; i++) {
if (!isAlive(pid)) {
log.info("{} stopped", bisqAppConfig.appName);
break;
}
MILLISECONDS.sleep(2500);
}
if (isAlive(pid)) {
this.shutdownExceptions.add(new IllegalStateException(format("%s shutdown did not work", bisqAppConfig.appName)));
}
} catch (Exception e) {
this.shutdownExceptions.add(new IllegalStateException(format("Error shutting down %s", bisqAppConfig.appName), e));
}
}
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;
}
}
// 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 startupScriptPath = config.rootProjectDir
+ "/" + bisqAppConfig.startupScript;
String bisqCmd = getJavaOptsSpec()
+ " " + startupScriptPath
+ " " + 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.runInBackground();
if (bashCommand.getExitStatus() != 0)
throw new IllegalStateException(format("Error starting BisqApp%n%s%nError: %s",
bisqAppConfig.appName,
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 + debugOpts + "\"; ";
}
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,182 @@
/*
* 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;
private String errorMessage;
public BitcoinCli(ApiTestConfig config, String command) {
super("bitcoin-cli", config);
this.command = command;
this.error = false;
this.errorMessage = null;
}
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;
}
public String getErrorMessage() {
return errorMessage;
}
@Override
public void start() throws InterruptedException, IOException {
verifyBitcoinPathsExist(false);
verifyBitcoindRunning();
commandWithOptions = config.bitcoinPath + "/bitcoin-cli -regtest "
+ " -rpcport=" + config.bitcoinRpcPort
+ " -rpcuser=" + config.bitcoinRpcUser
+ " -rpcpassword=" + config.bitcoinRpcPassword
+ " " + command;
BashCommand bashCommand = new BashCommand(commandWithOptions).run();
error = bashCommand.getExitStatus() != 0;
if (error) {
errorMessage = bashCommand.getError();
if (errorMessage == null || errorMessage.isEmpty())
throw new IllegalStateException("bitcoin-cli returned an error without a message");
} else {
output = bashCommand.getOutput();
}
}
@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,117 @@
/*
* 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.lang.String.format;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static joptsimple.internal.Strings.EMPTY;
import bisq.apitest.config.ApiTestConfig;
@Slf4j
public class BitcoinDaemon extends AbstractLinuxProcess implements LinuxProcess {
public BitcoinDaemon(ApiTestConfig config) {
super("bitcoind", config);
}
@Override
public void start() throws InterruptedException, IOException {
// If the bitcoind binary is dynamically linked to berkeley db libs, export the
// configured berkeley-db lib path. If statically linked, the berkeley db lib
// path will not be exported.
String berkeleyDbLibPathExport = config.berkeleyDbLibPath.equals(EMPTY) ? EMPTY
: "export LD_LIBRARY_PATH=" + config.berkeleyDbLibPath + "; ";
String bitcoindCmd = berkeleyDbLibPathExport
+ config.bitcoinPath + "/bitcoind"
+ " -datadir=" + config.bitcoinDatadir
+ " -daemon"
+ " -regtest=1"
+ " -server=1"
+ " -txindex=1"
+ " -peerbloomfilters=1"
+ " -debug=net"
+ " -fallbackfee=0.0002"
+ " -rpcport=" + config.bitcoinRpcPort
+ " -rpcuser=" + config.bitcoinRpcUser
+ " -rpcpassword=" + config.bitcoinRpcPassword
+ " -blocknotify=" + "\"" + config.bitcoinDatadir + "/blocknotify" + " %s\"";
BashCommand cmd = new BashCommand(bitcoindCmd).run();
log.info("Starting ...\n$ {}", cmd.getCommand());
if (cmd.getExitStatus() != 0) {
startupExceptions.add(new IllegalStateException(
format("Error starting bitcoind%nstatus: %d%nerror msg: %s",
cmd.getExitStatus(), cmd.getError())));
return;
}
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() {
try {
log.info("Shutting down bitcoind daemon...");
if (!isAlive(pid)) {
this.shutdownExceptions.add(new IllegalStateException("Bitcoind already shut down."));
return;
}
if (new BashCommand("kill -15 " + pid).run().getExitStatus() != 0) {
this.shutdownExceptions.add(new IllegalStateException("Could not shut down bitcoind; probably already stopped."));
return;
}
MILLISECONDS.sleep(2500); // allow it time to shutdown
if (isAlive(pid)) {
this.shutdownExceptions.add(new IllegalStateException(
format("Could not kill bitcoind process with pid %d.", pid)));
return;
}
log.info("Stopped");
} catch (InterruptedException ignored) {
// empty
} catch (IOException e) {
this.shutdownExceptions.add(new IllegalStateException("Error shutting down bitcoind.", e));
}
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.List;
public interface LinuxProcess {
void start() throws InterruptedException, IOException;
String getName();
long getPid();
boolean hasStartupExceptions();
boolean hasShutdownExceptions();
void logExceptions(List<Throwable> exceptions, org.slf4j.Logger log);
List<Throwable> getStartupExceptions();
List<Throwable> getShutdownExceptions();
void shutdown();
}

View file

@ -0,0 +1,121 @@
/*
* 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.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 ThreadedStreamHandler inputStreamHandler;
private ThreadedStreamHandler errorStreamHandler;
public SystemCommandExecutor(final List<String> cmdOptions) {
if (log.isDebugEnabled())
log.debug("cmd options {}", cmdOptions.toString());
if (cmdOptions.isEmpty())
throw new IllegalStateException("No command params specified.");
if (cmdOptions.contains("sudo"))
throw new IllegalStateException("'sudo' commands are prohibited.");
this.cmdOptions = cmdOptions;
}
// 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 exec() throws IOException, InterruptedException {
return exec(true);
}
// 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
// if the waitOnErrStream flag is true, else the method will not wait on (join)
// the error stream handler thread.
public int exec(boolean waitOnErrStream) throws IOException, InterruptedException {
Process process = new ProcessBuilder(cmdOptions).start();
// 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.
inputStreamHandler = new ThreadedStreamHandler(inputStream);
errorStreamHandler = new ThreadedStreamHandler(errorStream);
inputStreamHandler.start();
errorStreamHandler.start();
int exitStatus = process.waitFor();
inputStreamHandler.interrupt();
errorStreamHandler.interrupt();
inputStreamHandler.join();
if (waitOnErrStream)
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,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.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
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;
final StringBuilder outputBuffer = new StringBuilder();
ThreadedStreamHandler(InputStream inputStream) {
this.inputStream = inputStream;
}
public void run() {
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) {
// empty
}
}
public StringBuilder getOutputBuffer() {
return outputBuffer;
}
}