Tighten up error handling

This commit factors out a run() method in CliMain that either returns
(void) or throws an exception. This eliminates the need to call
System.exit in so many places as were previously. Indeed, now there is
only one place where System.exit is called.

It also removes the duplication of printing "Error: ..." to stderr when
something goes wrong by doing this once in the global catch clause in
the main method.
This commit is contained in:
Chris Beams 2020-05-04 13:58:46 +02:00
parent b0e5da8e2e
commit d48f9eb6f0
No known key found for this signature in database
GPG Key ID: 3D214F8F5BC5ED73

View File

@ -29,7 +29,6 @@ import bisq.proto.grpc.WalletGrpc;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
@ -45,6 +44,7 @@ import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import static java.lang.String.format;
import static java.lang.System.err;
import static java.lang.System.exit;
import static java.lang.System.out;
@ -55,9 +55,6 @@ import static java.lang.System.out;
@Slf4j
public class CliMain {
private static final int EXIT_SUCCESS = 0;
private static final int EXIT_FAILURE = 1;
private enum Method {
getversion,
getbalance,
@ -68,6 +65,15 @@ public class CliMain {
}
public static void main(String[] args) {
try {
run(args);
} catch (Throwable t) {
err.println("Error: " + t.getMessage());
exit(1);
}
}
public static void run(String[] args) {
var parser = new OptionParser();
var helpOpt = parser.accepts("help", "Print this help text")
@ -85,43 +91,33 @@ public class CliMain {
var passwordOpt = parser.accepts("password", "rpc server password")
.withRequiredArg();
OptionSet options = null;
try {
options = parser.parse(args);
} catch (OptionException ex) {
err.println("Error: " + ex.getMessage());
exit(EXIT_FAILURE);
}
OptionSet options = parser.parse(args);
if (options.has(helpOpt)) {
printHelp(parser, out);
exit(EXIT_SUCCESS);
return;
}
@SuppressWarnings("unchecked")
var nonOptionArgs = (List<String>) options.nonOptionArguments();
if (nonOptionArgs.isEmpty()) {
printHelp(parser, err);
err.println("Error: no method specified");
exit(EXIT_FAILURE);
throw new IllegalArgumentException("no method specified");
}
var methodName = nonOptionArgs.get(0);
Method method = null;
final Method method;
try {
method = Method.valueOf(methodName);
} catch (IllegalArgumentException ex) {
err.printf("Error: '%s' is not a supported method\n", methodName);
exit(EXIT_FAILURE);
throw new IllegalArgumentException(format("'%s' is not a supported method", methodName));
}
var host = options.valueOf(hostOpt);
var port = options.valueOf(portOpt);
var password = options.valueOf(passwordOpt);
if (password == null) {
err.println("Error: missing required 'password' option");
exit(EXIT_FAILURE);
}
if (password == null)
throw new IllegalArgumentException("missing required 'password' option");
var credentials = new PasswordCallCredentials(password);
@ -130,8 +126,7 @@ public class CliMain {
try {
channel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
ex.printStackTrace(err);
exit(EXIT_FAILURE);
throw new RuntimeException(ex);
}
}));
@ -144,76 +139,74 @@ public class CliMain {
var request = GetVersionRequest.newBuilder().build();
var version = versionService.getVersion(request).getVersion();
out.println(version);
exit(EXIT_SUCCESS);
return;
}
case getbalance: {
var request = GetBalanceRequest.newBuilder().build();
var reply = walletService.getBalance(request);
out.println(formatBalance(reply.getBalance()));
exit(EXIT_SUCCESS);
var satoshiBalance = reply.getBalance();
var satoshiDivisor = new BigDecimal(100000000);
var btcFormat = new DecimalFormat("###,##0.00000000");
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
var btcBalance = btcFormat.format(BigDecimal.valueOf(satoshiBalance).divide(satoshiDivisor));
out.println(btcBalance);
return;
}
case lockwallet: {
var request = LockWalletRequest.newBuilder().build();
walletService.lockWallet(request);
out.println("wallet locked");
exit(EXIT_SUCCESS);
return;
}
case unlockwallet: {
if (nonOptionArgs.size() < 2) {
err.println("Error: no \"password\" specified");
exit(EXIT_FAILURE);
}
if (nonOptionArgs.size() < 3) {
err.println("Error: no unlock timeout specified");
exit(EXIT_FAILURE);
}
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no password specified");
if (nonOptionArgs.size() < 3)
throw new IllegalArgumentException("no unlock timeout specified");
long timeout = 0;
try {
timeout = Long.parseLong(nonOptionArgs.get(2));
} catch (NumberFormatException e) {
err.println(nonOptionArgs.get(2) + " is not a number");
exit(EXIT_FAILURE);
throw new IllegalArgumentException(format("'%s' is not a number", nonOptionArgs.get(2)));
}
var request = UnlockWalletRequest.newBuilder()
.setPassword(nonOptionArgs.get(1))
.setTimeout(timeout).build();
walletService.unlockWallet(request);
out.println("wallet unlocked");
exit(EXIT_SUCCESS);
return;
}
case removewalletpassword: {
if (nonOptionArgs.size() < 2) {
err.println("Error: no \"password\" specified");
exit(EXIT_FAILURE);
}
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no password specified");
var request = RemoveWalletPasswordRequest.newBuilder().setPassword(nonOptionArgs.get(1)).build();
walletService.removeWalletPassword(request);
out.println("wallet decrypted");
exit(EXIT_SUCCESS);
return;
}
case setwalletpassword: {
if (nonOptionArgs.size() < 2) {
err.println("Error: no \"password\" specified");
exit(EXIT_FAILURE);
}
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no password specified");
var requestBuilder = SetWalletPasswordRequest.newBuilder().setPassword(nonOptionArgs.get(1));
var hasNewPassword = nonOptionArgs.size() == 3;
if (hasNewPassword)
requestBuilder.setNewPassword(nonOptionArgs.get(2));
walletService.setWalletPassword(requestBuilder.build());
out.println("wallet encrypted" + (hasNewPassword ? " with new password" : ""));
exit(EXIT_SUCCESS);
return;
}
default: {
err.printf("Error: unhandled method '%s'\n", method);
exit(EXIT_FAILURE);
}
throw new RuntimeException(format("unhandled method '%s'", method));
}
}
} catch (StatusRuntimeException ex) {
// Remove the leading gRPC status code (e.g. "UNKNOWN: ") from the message
String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", "");
err.println("Error: " + message);
exit(EXIT_FAILURE);
throw new RuntimeException(message, ex);
}
}
@ -230,19 +223,13 @@ public class CliMain {
stream.format("%-19s%-30s%s%n", "getversion", "", "Get server version");
stream.format("%-19s%-30s%s%n", "getbalance", "", "Get server wallet balance");
stream.format("%-19s%-30s%s%n", "lockwallet", "", "Remove wallet password from memory, locking the wallet");
stream.format("%-19s%-30s%s%n", "unlockwallet", "\"password\" timeout", "Store wallet password in memory for 'timeout' seconds");
stream.format("%-19s%-30s%s%n", "setwalletpassword", "\"password\" [,\"newpassword\"]",
stream.format("%-19s%-30s%s%n", "unlockwallet", "password timeout",
"Store wallet password in memory for timeout seconds");
stream.format("%-19s%-30s%s%n", "setwalletpassword", "password [newpassword]",
"Encrypt wallet with password, or set new password on encrypted wallet");
stream.println();
} catch (IOException ex) {
ex.printStackTrace(stream);
}
}
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
private static String formatBalance(long satoshis) {
var btcFormat = new DecimalFormat("###,##0.00000000");
var satoshiDivisor = new BigDecimal(100000000);
return btcFormat.format(BigDecimal.valueOf(satoshis).divide(satoshiDivisor));
}
}