mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-07-21 06:11:46 -04:00
Bisq
This commit is contained in:
commit
8a38081c04
2800 changed files with 344130 additions and 0 deletions
80
apitest/src/main/java/bisq/apitest/ApiTestMain.java
Normal file
80
apitest/src/main/java/bisq/apitest/ApiTestMain.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
469
apitest/src/main/java/bisq/apitest/Scaffold.java
Normal file
469
apitest/src/main/java/bisq/apitest/Scaffold.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
85
apitest/src/main/java/bisq/apitest/SetupTask.java
Normal file
85
apitest/src/main/java/bisq/apitest/SetupTask.java
Normal 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 + "]";
|
||||
}
|
||||
}
|
||||
}
|
51
apitest/src/main/java/bisq/apitest/SmokeTestBashCommand.java
Normal file
51
apitest/src/main/java/bisq/apitest/SmokeTestBashCommand.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
72
apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java
Normal file
72
apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java
Normal 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());
|
||||
}
|
||||
}
|
380
apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java
Normal file
380
apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java
Normal 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);
|
||||
}
|
||||
}
|
133
apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java
Normal file
133
apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java
Normal 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" +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
156
apitest/src/main/java/bisq/apitest/linux/BashCommand.java
Normal file
156
apitest/src/main/java/bisq/apitest/linux/BashCommand.java
Normal 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");
|
||||
}
|
||||
}
|
266
apitest/src/main/java/bisq/apitest/linux/BisqProcess.java
Normal file
266
apitest/src/main/java/bisq/apitest/linux/BisqProcess.java
Normal 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");
|
||||
}
|
||||
}
|
182
apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java
Normal file
182
apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java
Normal 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");
|
||||
}
|
||||
}
|
117
apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java
Normal file
117
apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
42
apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java
Normal file
42
apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java
Normal 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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue