mirror of
https://github.com/bisq-network/bisq-api-reference.git
synced 2026-01-29 18:03:34 +00:00
667 lines
27 KiB
Java
667 lines
27 KiB
Java
/*
|
|
* 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.bots;
|
|
|
|
import bisq.bots.table.builder.TableBuilder;
|
|
import bisq.proto.grpc.GetTradesRequest;
|
|
import bisq.proto.grpc.OfferInfo;
|
|
import bisq.proto.grpc.TradeInfo;
|
|
import io.grpc.Status;
|
|
import io.grpc.StatusRuntimeException;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.slf4j.Logger;
|
|
import protobuf.PaymentAccount;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.math.BigDecimal;
|
|
import java.math.MathContext;
|
|
import java.time.Duration;
|
|
import java.util.*;
|
|
import java.util.function.*;
|
|
|
|
import static bisq.bots.CurrencyFormat.toSatoshis;
|
|
import static bisq.bots.table.builder.TableType.*;
|
|
import static java.lang.String.format;
|
|
import static java.lang.System.*;
|
|
import static java.math.BigDecimal.ZERO;
|
|
import static java.math.RoundingMode.HALF_UP;
|
|
|
|
/**
|
|
* Convenience methods and functions not depending on a bot's state nor the need to send requests to the API daemon.
|
|
*/
|
|
@Slf4j
|
|
public class BotUtils {
|
|
|
|
private static final String BANNER = "##############################################################################";
|
|
|
|
public static final Predicate<String> isBsq = (currencyCode) -> currencyCode.equalsIgnoreCase("BSQ");
|
|
public static final Predicate<String> isXmr = (currencyCode) -> currencyCode.equalsIgnoreCase("XMR");
|
|
public static final Predicate<String> isAltcoin = (currencyCode) -> isBsq.test(currencyCode) || isXmr.test(currencyCode);
|
|
|
|
/**
|
|
* Return price precision of 8 for altcoin, 4 for fiat.
|
|
*/
|
|
public static final Function<String, Integer> toPricePrecision = (currencyCode) ->
|
|
isAltcoin.test(currencyCode) ? 8 : 4;
|
|
|
|
/**
|
|
* Calculates a target price from given max market price margin and market price.
|
|
*
|
|
* @param targetMarketPriceMargin the maximum or minimum market price margin
|
|
* @param currentMarketPrice the current market price
|
|
* @param currencyCode the asset currency code (calculated price precision = 4 for fiat, 8 for altcoin)
|
|
* @return BigDecimal a target price
|
|
*/
|
|
public static BigDecimal calcTargetPrice(BigDecimal targetMarketPriceMargin,
|
|
BigDecimal currentMarketPrice,
|
|
String currencyCode) {
|
|
if (!isZero.test(targetMarketPriceMargin) && targetMarketPriceMargin.precision() < 2)
|
|
throw new IllegalArgumentException(
|
|
format("Price margin percent literal argument %s is invalid;"
|
|
+ " it must have a precision of at least 2 decimal places.",
|
|
targetMarketPriceMargin));
|
|
|
|
var maxMarketPriceMarginAsDecimal = scaleAsDecimal.apply(targetMarketPriceMargin);
|
|
var precision = toPricePrecision.apply(currencyCode);
|
|
return currentMarketPrice.add(currentMarketPrice
|
|
.multiply(maxMarketPriceMarginAsDecimal, new MathContext(precision, HALF_UP)))
|
|
.setScale(precision, HALF_UP);
|
|
}
|
|
|
|
/**
|
|
* Calculates a target BSQ price from given max market price margin and an average BSQ market price.
|
|
*
|
|
* @param targetMarketPriceMargin the maximum or minimum market price margin
|
|
* @param avgBsqPrice the average BSQ price
|
|
* @return BigDecimal a target price
|
|
*/
|
|
public static BigDecimal calcTargetBsqPrice(BigDecimal targetMarketPriceMargin,
|
|
BigDecimal avgBsqPrice) {
|
|
if (!isZero.test(targetMarketPriceMargin) && targetMarketPriceMargin.precision() <= 2)
|
|
throw new IllegalArgumentException(
|
|
format("Price margin percent literal argument %s is invalid;"
|
|
+ " it must have a precision of at least 2 decimal places.",
|
|
targetMarketPriceMargin));
|
|
|
|
var maxMarketPriceMarginAsDecimal = scaleAsDecimal.apply(targetMarketPriceMargin);
|
|
return avgBsqPrice.add(avgBsqPrice
|
|
.multiply(maxMarketPriceMarginAsDecimal, new MathContext(8, HALF_UP)))
|
|
.setScale(8, HALF_UP);
|
|
}
|
|
|
|
/**
|
|
* Convert milliseconds to seconds.
|
|
*/
|
|
public static final Function<Long, Long> toSeconds = (ms) -> Duration.ofMillis(ms).getSeconds();
|
|
|
|
/**
|
|
* Return true if given BigDecimal equals 0.00.
|
|
*/
|
|
public static final Predicate<BigDecimal> isZero = (d) -> d.compareTo(ZERO) == 0;
|
|
|
|
/**
|
|
* Convert a BigDecimal representing a % literal to a BigDecimal representing
|
|
* the equivalent decimal, e.g., 1.00 (%) converts to 0.01.
|
|
*/
|
|
public static final Function<BigDecimal, BigDecimal> scaleAsDecimal = (pctLiteral) ->
|
|
pctLiteral.divide(new BigDecimal("100"), HALF_UP);
|
|
|
|
/**
|
|
* Return a BigDecimal representing the difference as a percentage between a base number and n,
|
|
* i.e., how much above or below (as a %) is n compared to base?
|
|
*/
|
|
public static final BiFunction<BigDecimal, BigDecimal, BigDecimal> diffAsPercent = (base, n) -> {
|
|
BigDecimal factor = new BigDecimal("100");
|
|
BigDecimal diff = n.divide(base, 4, HALF_UP).multiply(factor);
|
|
return diff.subtract(factor);
|
|
};
|
|
|
|
|
|
/**
|
|
* Return true if the offer's margin based price >= target price.
|
|
*/
|
|
public static final BiPredicate<OfferInfo, BigDecimal> isMarginBasedPriceGETargetPrice =
|
|
(offer, targetPrice) -> offer.getUseMarketBasedPrice()
|
|
&& new BigDecimal(offer.getPrice()).compareTo(targetPrice) >= 0;
|
|
|
|
/**
|
|
* Return true if the margin price based offer's market price margin (%) <= maxMarketPriceMargin (%).
|
|
*/
|
|
public static final BiPredicate<OfferInfo, BigDecimal> isMarginLEMaxMarketPriceMargin =
|
|
(offer, maxMarketPriceMargin) -> offer.getUseMarketBasedPrice()
|
|
&& offer.getMarketPriceMarginPct() <= maxMarketPriceMargin.doubleValue();
|
|
|
|
/**
|
|
* Return true is fixed-price offer's price <= the bot's max market price margin. Allows bot to
|
|
* take a fixed-priced offer if the price is <= maxMarketPriceMargin (%) of the current market price.
|
|
*/
|
|
public static boolean isFixedPriceLEMaxMarketPriceMargin(OfferInfo offer,
|
|
BigDecimal currentMarketPrice,
|
|
BigDecimal maxMarketPriceMargin) {
|
|
if (offer.getUseMarketBasedPrice())
|
|
return false;
|
|
|
|
BigDecimal offerPrice = new BigDecimal(offer.getPrice());
|
|
|
|
// How much above or below currentMarketPrice (as a %) is the offer's fixed-price?
|
|
BigDecimal distanceFromMarketPrice = diffAsPercent.apply(currentMarketPrice, offerPrice);
|
|
|
|
// Return true if distanceFromMarketPrice <= maxMarketPriceMargin.
|
|
return distanceFromMarketPrice.compareTo(maxMarketPriceMargin) <= 0;
|
|
}
|
|
|
|
/**
|
|
* Return true is fixed-price offer's price >= the bot's minimum market price margin. Allows bot to
|
|
* take a fixed-priced offer if the price is >= minMarketPriceMargin (%) of the current market price.
|
|
*/
|
|
public static boolean isFixedPriceGEMinMarketPriceMargin(OfferInfo offer,
|
|
BigDecimal currentMarketPrice,
|
|
BigDecimal minMarketPriceMargin) {
|
|
if (offer.getUseMarketBasedPrice())
|
|
return false;
|
|
|
|
BigDecimal offerPrice = new BigDecimal(offer.getPrice());
|
|
|
|
// How much above or below currentMarketPrice (as a %) is the offer's fixed-price?
|
|
BigDecimal distanceFromMarketPrice = diffAsPercent.apply(currentMarketPrice, offerPrice);
|
|
|
|
// Return true if distanceFromMarketPrice <= maxMarketPriceMargin.
|
|
return distanceFromMarketPrice.compareTo(minMarketPriceMargin) >= 0;
|
|
}
|
|
|
|
/**
|
|
* Return String "below" if maxMarketPriceMargin (%) <= 0.00, else "above".
|
|
*/
|
|
public static final Function<BigDecimal, String> aboveOrBelowMarketPrice = (maxMarketPriceMargin) ->
|
|
maxMarketPriceMargin.compareTo(ZERO) <= 0 ? "below" : "above";
|
|
|
|
/**
|
|
* Return true if offer.amt >= minAmount AND offer.amt <= maxAmount (within the boundaries).
|
|
* TODO API's takeoffer needs to support taking offer's minAmount.
|
|
*/
|
|
public static boolean isWithinBTCAmountBounds(OfferInfo offer, BigDecimal minAmount, BigDecimal maxAmount) {
|
|
return offer.getAmount() >= toSatoshis(minAmount) && offer.getAmount() <= toSatoshis(maxAmount);
|
|
}
|
|
|
|
/**
|
|
* Return true if the given StatusRuntimeException's Status matches the given Status.
|
|
*/
|
|
public static final BiPredicate<StatusRuntimeException, Status> exceptionHasStatus = (ex, status) ->
|
|
ex.getStatus().getCode() == status.getCode();
|
|
|
|
/**
|
|
* Return a StatusRuntimeException message stripped of it's leading Status Code's enum name.
|
|
*/
|
|
public static final Function<StatusRuntimeException, String> toCleanErrorMessage = (grpcException) ->
|
|
grpcException.getMessage().replaceFirst("^[A-Z_]+: ", "");
|
|
|
|
/**
|
|
* Return a StatusRuntimeException message stripped of it's leading Status Code's enum name,
|
|
* then prepended with String "Non-Fatal Error (", and appended with String ")".
|
|
*/
|
|
public static final Function<StatusRuntimeException, String> toNonFatalErrorMessage = (grpcException) ->
|
|
"Non-Fatal Error (" + toCleanErrorMessage.apply(grpcException) + ")";
|
|
|
|
/**
|
|
* Return true if the given offer's payment method matches the given payment account's payment method.
|
|
*/
|
|
public static final BiPredicate<OfferInfo, PaymentAccount> usesSamePaymentMethod = (o, p) ->
|
|
o.getPaymentMethodId().equals(p.getPaymentMethod().getId());
|
|
|
|
/**
|
|
* Return true if I am the BTC buyer for the given trade.
|
|
*/
|
|
public static final Predicate<TradeInfo> isBtcBuyer = (trade) -> {
|
|
var isMyOffer = trade.getOffer().getIsMyOffer();
|
|
var isBuyerMakerAndSellerTaker = trade.getContract().getIsBuyerMakerAndSellerTaker();
|
|
return isMyOffer == isBuyerMakerAndSellerTaker;
|
|
};
|
|
|
|
/**
|
|
* Put the current thread to sleep for given number of milliseconds.
|
|
*/
|
|
public static void sleep(long ms) {
|
|
try {
|
|
Thread.sleep(ms);
|
|
} catch (InterruptedException ex) {
|
|
// ignored
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a wallet password read from stdin. If read from a command terminal, input will not be echoed.
|
|
* If run in a virtual terminal (IDE console), the input will be echoed.
|
|
*
|
|
* @param prompt password prompt
|
|
* @return String the password
|
|
*/
|
|
public static String readWalletPassword(String prompt) {
|
|
String walletPassword;
|
|
var console = console();
|
|
// System.console() returns null if you do not launch your java application with a real console.
|
|
if (console == null) {
|
|
// Have to read it in a less secure way in the IDE's virtual console.
|
|
out.println(prompt);
|
|
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
|
|
try {
|
|
walletPassword = reader.readLine();
|
|
} catch (IOException ex) {
|
|
throw new RuntimeException(ex);
|
|
}
|
|
} else {
|
|
char[] pwdChars = console.readPassword(prompt);
|
|
walletPassword = new String(pwdChars);
|
|
}
|
|
return walletPassword;
|
|
}
|
|
|
|
/**
|
|
* Return the given String[] args with an additional --wallet-password=xyz option appended to it.
|
|
*
|
|
* @param args program arguments
|
|
* @param walletPassword wallet password
|
|
* @return String[] appended program arguments
|
|
*/
|
|
public static String[] appendWalletPasswordOpt(String[] args, String walletPassword) {
|
|
String[] walletPasswordOpt = new String[]{"--wallet-password=" + walletPassword};
|
|
String[] newOpts = new String[args.length + 1];
|
|
arraycopy(args, 0, newOpts, 0, args.length);
|
|
arraycopy(walletPasswordOpt, 0, newOpts, args.length, walletPasswordOpt.length);
|
|
return newOpts;
|
|
}
|
|
|
|
/**
|
|
* Returns a validated address:port specification as a String.
|
|
*
|
|
* @param addressString The address:port pair being validated.
|
|
* @return String
|
|
*/
|
|
public static String getValidatedPeerAddress(String addressString) {
|
|
String[] hostnameAndPort = addressString.split(":");
|
|
String hostname;
|
|
int port;
|
|
try {
|
|
if (hostnameAndPort.length < 2) {
|
|
throw new IllegalStateException(format("Invalid preferredTradingPeers configuration:%n"
|
|
+ "\t\t%s%n\tEach address much include a port, i.e, host:port.",
|
|
addressString));
|
|
}
|
|
hostname = hostnameAndPort[0].trim();
|
|
port = Integer.parseInt(hostnameAndPort[1].trim());
|
|
} catch (Exception ex) {
|
|
throw new IllegalStateException(format("Invalid preferredTradingPeers configuration:%n"
|
|
+ "\t\t%s%n\tMultiple addresses must be separated by commas.",
|
|
addressString),
|
|
ex);
|
|
}
|
|
return hostname + ":" + port;
|
|
}
|
|
|
|
/**
|
|
* Return given Map transformed into a String representing a table with two columns: label and value.
|
|
* <p>
|
|
* The map argument should contain only scalar values or short strings as values
|
|
* (not lists or maps), or you will get ugly results.
|
|
*/
|
|
public static final BiFunction<String, Map<String, Object>, String> toTable = (title, map) -> {
|
|
var mapElements = map.entrySet();
|
|
Supplier<Integer> longestLabel = () -> {
|
|
int[] len = {0}; // Make implicitly final to modify in map element iteration.
|
|
mapElements.forEach((e) -> {
|
|
var labelLen = e.getKey().length();
|
|
len[0] = Math.max(labelLen, len[0]);
|
|
});
|
|
return len[0];
|
|
};
|
|
int labelWidth = longestLabel.get() + 2;
|
|
Supplier<String> resultsTable = () -> {
|
|
var numRows = mapElements.size();
|
|
var rows = new StringBuilder();
|
|
int[] rowNum = {0}; // Make implicitly final to modify in map element iteration.
|
|
mapElements.forEach((e) -> {
|
|
var label = e.getKey();
|
|
String value;
|
|
if (e.getValue() instanceof Boolean) {
|
|
value = ((Boolean) e.getValue()) ? "YES" : "NO";
|
|
} else {
|
|
value = e.getValue().toString();
|
|
}
|
|
var rowFormatSpec = (label.startsWith("\t"))
|
|
? "%-" + labelWidth + "s" + " " + "%s"
|
|
: "%-" + (labelWidth + 3) + "s" + " " + "%s";
|
|
var row = format(rowFormatSpec, label, value);
|
|
rows.append("\t").append(row);
|
|
if (++rowNum[0] < numRows) {
|
|
rows.append("\n");
|
|
}
|
|
});
|
|
return rows.toString();
|
|
};
|
|
return title + "\n" +
|
|
resultsTable.get();
|
|
};
|
|
|
|
/**
|
|
* Print offer summary to stdout.
|
|
*
|
|
* @param offer printed offer
|
|
*/
|
|
public static void printOfferSummary(OfferInfo offer) {
|
|
new TableBuilder(OFFER_TBL, offer).build().print(out);
|
|
}
|
|
|
|
/**
|
|
* Print list of offer summaries to stdout
|
|
*
|
|
* @param offers printed offer list
|
|
*/
|
|
public static void printOffersSummary(List<OfferInfo> offers) {
|
|
new TableBuilder(OFFER_TBL, offers).build().print(out);
|
|
}
|
|
|
|
/**
|
|
* Print trade summary to stdout.
|
|
*
|
|
* @param trade printed trade
|
|
*/
|
|
public static void printTradeSummary(TradeInfo trade) {
|
|
new TableBuilder(TRADE_DETAIL_TBL, trade).build().print(out);
|
|
}
|
|
|
|
/**
|
|
* Print list of trade summaries to stdout.
|
|
*
|
|
* @param category category OPEN | CLOSED | FAILED
|
|
* @param trades list of trades
|
|
*/
|
|
public static void printTradesSummary(GetTradesRequest.Category category, List<TradeInfo> trades) {
|
|
log.info("{} trades:", category.name());
|
|
switch (category) {
|
|
case CLOSED -> new TableBuilder(CLOSED_TRADES_TBL, trades).build().print(out);
|
|
case FAILED -> new TableBuilder(FAILED_TRADES_TBL, trades).build().print(out);
|
|
default -> new TableBuilder(OPEN_TRADES_TBL, trades).build().print(out);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prints PaymentAccount summary to stdout.
|
|
*
|
|
* @param paymentAccount the printed PaymentAccount
|
|
*/
|
|
public static void printPaymentAccountSummary(PaymentAccount paymentAccount) {
|
|
new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out);
|
|
}
|
|
|
|
/**
|
|
* Log a CLI confirmpaymentstarted command for a simulated trading peer.
|
|
*
|
|
* @param log calling bot's logger
|
|
* @param tradingPeerApiPassword trading peer's CLI --password param value
|
|
* @param tradingPeerApiPort trading peer's CLI --port param value
|
|
* @param tradeId trade's unique identifier (cannot be short-id)
|
|
*/
|
|
public static void printCliPaymentStartedCommand(Logger log,
|
|
String tradingPeerApiPassword,
|
|
int tradingPeerApiPort,
|
|
String currencyCode,
|
|
String tradeId) {
|
|
log.warn(BANNER);
|
|
log.warn("BTC buyer must manually confirm {} payment has been sent"
|
|
+ " with a confirmpaymentstarted CLI command:",
|
|
currencyCode);
|
|
log.warn("./bisq-cli --password={} --port={} confirmpaymentstarted --trade-id={}",
|
|
tradingPeerApiPassword,
|
|
tradingPeerApiPort,
|
|
tradeId);
|
|
log.warn(BANNER);
|
|
}
|
|
|
|
/**
|
|
* Log a CLI confirmpaymentreceived command for a simulated trading peer.
|
|
*
|
|
* @param log calling bot's logger
|
|
* @param tradingPeerApiPassword trading peer's CLI --password param value
|
|
* @param tradingPeerApiPort trading peer's CLI --port param value
|
|
* @param tradeId trade's unique identifier (cannot be short-id)
|
|
*/
|
|
public static void printCliPaymentReceivedConfirmationCommand(Logger log,
|
|
String tradingPeerApiPassword,
|
|
int tradingPeerApiPort,
|
|
String currencyCode,
|
|
String tradeId) {
|
|
log.warn(BANNER);
|
|
log.warn("BTC seller must manually confirm {} payment was received"
|
|
+ " with a confirmpaymentreceived CLI command:",
|
|
currencyCode);
|
|
log.warn("./bisq-cli --password={} --port={} confirmpaymentreceived --trade-id={}",
|
|
tradingPeerApiPassword,
|
|
tradingPeerApiPort,
|
|
tradeId);
|
|
log.warn(BANNER);
|
|
}
|
|
|
|
/**
|
|
* Log a CLI closetrade command for a simulated trading peer.
|
|
*
|
|
* @param log calling bot's logger
|
|
* @param tradingPeerApiPassword trading peer's CLI --password param value
|
|
* @param tradingPeerApiPort trading peer's CLI --port param value
|
|
* @param tradeId trade's unique identifier (cannot be short-id)
|
|
*/
|
|
public static void printCliCloseTradeCommand(Logger log,
|
|
String tradingPeerApiPassword,
|
|
int tradingPeerApiPort,
|
|
String tradeId) {
|
|
log.warn(BANNER);
|
|
log.warn("Trading peer must manually close trade with a closetrade CLI command:");
|
|
log.warn("./bisq-cli --password={} --port={} closetrade --trade-id={}",
|
|
tradingPeerApiPassword,
|
|
tradingPeerApiPort,
|
|
tradeId);
|
|
log.warn(BANNER);
|
|
}
|
|
|
|
/**
|
|
* Log a CLI gettrades --category=closed command for a simulated trading peer.
|
|
*
|
|
* @param log calling bot's logger
|
|
* @param tradingPeerApiPassword trading peer's CLI --password param value
|
|
* @param tradingPeerApiPort trading peer's CLI --port param value
|
|
*/
|
|
public static void printCliGetClosedTradesCommand(Logger log,
|
|
String tradingPeerApiPassword,
|
|
int tradingPeerApiPort) {
|
|
log.warn(BANNER);
|
|
log.warn("Trading peer can view completed trade history with a gettrades CLI command:");
|
|
log.warn("./bisq-cli --password={} --port={} gettrades --category=closed",
|
|
tradingPeerApiPassword,
|
|
tradingPeerApiPort);
|
|
log.warn(BANNER);
|
|
}
|
|
|
|
/**
|
|
* Run a bash script to count down the given number of seconds, printing each character of output from stdout.
|
|
* <p>
|
|
* Can only be run if the system's bash command language interpreter can be found.
|
|
*
|
|
* @param seconds to count down
|
|
*/
|
|
public static void showCountdown(int seconds) {
|
|
getBashPath().ifPresentOrElse((bashPath) -> {
|
|
var bashScript = format(
|
|
"for i in {%d..1}; do echo -ne \"Waking up in $i seconds...\\r\" && sleep 1; done; echo -ne \"\\r\"", seconds);
|
|
try {
|
|
BotUtils.runBashCommand(bashScript);
|
|
} catch (IOException ex) {
|
|
throw new RuntimeException("Error running bash script.", ex);
|
|
} catch (InterruptedException ignored) {
|
|
// ignored
|
|
}
|
|
}, () -> {
|
|
throw new UnsupportedOperationException("Bash command language interpreter not found.");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Execute a bash system command, print process' stdout during the command's execution,
|
|
* and return its status code (0 or 1).
|
|
*
|
|
* @param bashCommand the system bash command
|
|
* @return int system command status code
|
|
* @throws IOException if an I/O error occurs
|
|
* @throws InterruptedException if the current thread is interrupted by another thread while it is waiting,
|
|
* then the wait is ended and an InterruptedException is thrown.
|
|
* @throws UnsupportedOperationException if the command language interpreter could not be found on the system, or
|
|
* if the operating system does not support the creation of processes.
|
|
*/
|
|
@SuppressWarnings("UnusedReturnValue")
|
|
public static int runBashCommand(String bashCommand) throws IOException, InterruptedException {
|
|
var bashPath = getBashPath();
|
|
if (bashPath.isPresent()) {
|
|
List<String> cmdOptions = new ArrayList<>() {{
|
|
//noinspection OptionalGetWithoutIsPresent
|
|
add(bashPath.get());
|
|
add("-c");
|
|
add(bashCommand);
|
|
}};
|
|
Process process = new ProcessBuilder(cmdOptions).start();
|
|
try (InputStreamReader isr = new InputStreamReader(process.getInputStream())) {
|
|
int c;
|
|
while ((c = isr.read()) >= 0) {
|
|
out.print((char) c);
|
|
out.flush();
|
|
}
|
|
}
|
|
return process.waitFor();
|
|
} else {
|
|
throw new UnsupportedOperationException("Bash util not found on this " + getOSName() + " system.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return an Optional<String> for the absolute path of the system's bash utility,
|
|
* if it exists at one of two locations: "/bin/bash", or "/usr/bin/bash".
|
|
*
|
|
* @return Optional<String>
|
|
*/
|
|
public static Optional<String> getBashPath() {
|
|
if (isUnix()) {
|
|
var f1 = new File("/bin/bash");
|
|
var f2 = new File("/usr/bin/bash");
|
|
if (f1.exists() && f1.canExecute()) {
|
|
return Optional.of(f1.getAbsolutePath());
|
|
} else if (f2.exists() && f2.canExecute()) {
|
|
return Optional.of(f2.getAbsolutePath());
|
|
} else {
|
|
return Optional.empty();
|
|
}
|
|
} else {
|
|
return Optional.empty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if OS is any flavor of Linux.
|
|
*
|
|
* @return true if OS is any flavor of Linux
|
|
*/
|
|
public static boolean isUnix() {
|
|
return isOSX() || isLinux() || getOSName().contains("freebsd");
|
|
}
|
|
|
|
/**
|
|
* Return true if OS is Windows.
|
|
*
|
|
* @return true if OS is Windows
|
|
*/
|
|
public static boolean isWindows() {
|
|
return getOSName().contains("win");
|
|
}
|
|
|
|
/**
|
|
* Return true if running on a virtualized OS within Qubes.
|
|
*
|
|
* @return true if running on a virtualized OS within Qubes
|
|
*/
|
|
public static boolean isQubesOS() {
|
|
// For Linux qubes, "os.version" looks like "4.19.132-1.pvops.qubes.x86_64"
|
|
// The presence of the "qubes" substring indicates this Linux is running as a qube
|
|
// This is the case for all 3 virtualization modes (PV, PVH, HVM)
|
|
// In addition, this works for both simple AppVMs, as well as for StandaloneVMs
|
|
// TODO This might not work for detecting Qubes virtualization for other OSes
|
|
// like Windows
|
|
return getOSVersion().contains("qubes");
|
|
}
|
|
|
|
/**
|
|
* Return true if OS is Mac.
|
|
*
|
|
* @return true if OS is Mac
|
|
*/
|
|
public static boolean isOSX() {
|
|
return getOSName().contains("mac") || getOSName().contains("darwin");
|
|
}
|
|
|
|
/**
|
|
* Return true if OS is Linux.
|
|
*
|
|
* @return true if OS is Linux
|
|
*/
|
|
public static boolean isLinux() {
|
|
return getOSName().contains("linux");
|
|
}
|
|
|
|
/**
|
|
* Return true if OS is Debian Linux.
|
|
*
|
|
* @return true if OS is Debian Linux
|
|
*/
|
|
public static boolean isDebianLinux() {
|
|
return isLinux() && new File("/etc/debian_version").isFile();
|
|
}
|
|
|
|
/**
|
|
* Return true if OS is Redhat Linux.
|
|
*
|
|
* @return true if OS is Redhat Linux
|
|
*/
|
|
public static boolean isRedHatLinux() {
|
|
return isLinux() && new File("/etc/redhat-release").isFile();
|
|
}
|
|
|
|
/**
|
|
* Returns the OS name in lower case.
|
|
*
|
|
* @return OS name
|
|
*/
|
|
public static String getOSName() {
|
|
return System.getProperty("os.name").toLowerCase(Locale.US);
|
|
}
|
|
|
|
/**
|
|
* Returns the OS version in lower case.
|
|
*
|
|
* @return OS version
|
|
*/
|
|
public static String getOSVersion() {
|
|
return System.getProperty("os.version").toLowerCase(Locale.US);
|
|
}
|
|
}
|