delete and restore account restarts application automatically

added standard input to keepalive loop for issuing commands to daemon

Co-authored-by: duriancrepe <duriancrepe@protonmail.com>
This commit is contained in:
woodser 2023-01-19 19:19:56 -05:00
parent 350d2d5398
commit 9877ba87a4
14 changed files with 181 additions and 73 deletions

View file

@ -108,6 +108,15 @@ public class PersistenceManager<T extends PersistableEnvelope> {
flushAllDataToDisk(completeHandler, true); flushAllDataToDisk(completeHandler, true);
} }
/**
* Resets the static members of PersistenceManager to restart the application.
*/
public static void reset() {
ALL_PERSISTENCE_MANAGERS.clear();
flushAtShutdownCalled = false;
allServicesInitialized.set(false);
}
// We require being called only once from the global shutdown routine. As the shutdown routine has a timeout // We require being called only once from the global shutdown routine. As the shutdown routine has a timeout
// and error condition where we call the method as well beside the standard path and it could be that those // and error condition where we call the method as well beside the standard path and it could be that those
// alternative code paths call our method after it was called already, so it is a valid but rare case. // alternative code paths call our method after it was called already, so it is a valid but rare case.

View file

@ -21,4 +21,5 @@ import bisq.common.handlers.ResultHandler;
public interface GracefulShutDownHandler { public interface GracefulShutDownHandler {
void gracefulShutDown(ResultHandler resultHandler); void gracefulShutDown(ResultHandler resultHandler);
void gracefulShutDown(ResultHandler resultHandler, boolean systemExit);
} }

View file

@ -69,12 +69,16 @@ public class CoreAccountService {
} }
public void addListener(AccountServiceListener listener) { public void addListener(AccountServiceListener listener) {
synchronized (listeners) {
listeners.add(listener); listeners.add(listener);
} }
}
public boolean removeListener(AccountServiceListener listener) { public boolean removeListener(AccountServiceListener listener) {
synchronized (listeners) {
return listeners.remove(listener); return listeners.remove(listener);
} }
}
public boolean accountExists() { public boolean accountExists() {
return keyStorage.allKeyFilesExist(); // public and private key pair indicate the existence of the account return keyStorage.allKeyFilesExist(); // public and private key pair indicate the existence of the account
@ -99,7 +103,9 @@ public class CoreAccountService {
if (!accountExists()) throw new IllegalStateException("Cannot open account if account does not exist"); if (!accountExists()) throw new IllegalStateException("Cannot open account if account does not exist");
if (keyRing.unlockKeys(password, false)) { if (keyRing.unlockKeys(password, false)) {
this.password = password; this.password = password;
for (AccountServiceListener listener : new ArrayList<AccountServiceListener>(listeners)) listener.onAccountOpened(); synchronized (listeners) {
for (AccountServiceListener listener : listeners) listener.onAccountOpened();
}
} else { } else {
throw new IllegalStateException("keyRing.unlockKeys() returned false, that should never happen"); throw new IllegalStateException("keyRing.unlockKeys() returned false, that should never happen");
} }
@ -110,13 +116,17 @@ public class CoreAccountService {
String oldPassword = this.password; String oldPassword = this.password;
keyStorage.saveKeyRing(keyRing, oldPassword, password); keyStorage.saveKeyRing(keyRing, oldPassword, password);
this.password = password; this.password = password;
for (AccountServiceListener listener : new ArrayList<AccountServiceListener>(listeners)) listener.onPasswordChanged(oldPassword, password); synchronized (listeners) {
for (AccountServiceListener listener : listeners) listener.onPasswordChanged(oldPassword, password);
}
} }
public void closeAccount() { public void closeAccount() {
if (!isAccountOpen()) throw new IllegalStateException("Cannot close unopened account"); if (!isAccountOpen()) throw new IllegalStateException("Cannot close unopened account");
keyRing.lockKeys(); // closed account means the keys are locked keyRing.lockKeys(); // closed account means the keys are locked
for (AccountServiceListener listener : new ArrayList<AccountServiceListener>(listeners)) listener.onAccountClosed(); synchronized (listeners) {
for (AccountServiceListener listener : listeners) listener.onAccountClosed();
}
} }
public void backupAccount(int bufferSize, Consumer<InputStream> consume, Consumer<Exception> error) { public void backupAccount(int bufferSize, Consumer<InputStream> consume, Consumer<Exception> error) {
@ -147,13 +157,17 @@ public class CoreAccountService {
if (accountExists()) throw new IllegalStateException("Cannot restore account if there is an existing account"); if (accountExists()) throw new IllegalStateException("Cannot restore account if there is an existing account");
File dataDir = new File(config.appDataDir.getPath()); File dataDir = new File(config.appDataDir.getPath());
ZipUtils.unzipToDir(dataDir, inputStream, bufferSize); ZipUtils.unzipToDir(dataDir, inputStream, bufferSize);
synchronized (listeners) {
for (AccountServiceListener listener : listeners) listener.onAccountRestored(onShutdown); for (AccountServiceListener listener : listeners) listener.onAccountRestored(onShutdown);
} }
}
public void deleteAccount(Runnable onShutdown) { public void deleteAccount(Runnable onShutdown) {
try { try {
keyRing.lockKeys(); keyRing.lockKeys();
synchronized (listeners) {
for (AccountServiceListener listener : listeners) listener.onAccountDeleted(onShutdown); for (AccountServiceListener listener : listeners) listener.onAccountDeleted(onShutdown);
}
File dataDir = new File(config.appDataDir.getPath()); // TODO (woodser): deleting directory after gracefulShutdown() so services don't throw when they try to persist (e.g. XmrTxProofService), but gracefulShutdown() should honor read-only shutdown File dataDir = new File(config.appDataDir.getPath()); // TODO (woodser): deleting directory after gracefulShutdown() so services don't throw when they try to persist (e.g. XmrTxProofService), but gracefulShutdown() should honor read-only shutdown
FileUtil.deleteDirectory(dataDir, null, false); FileUtil.deleteDirectory(dataDir, null, false);
} catch (Exception err) { } catch (Exception err) {

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>. * along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/ */
package bisq.daemon.app; package bisq.core.app;
import java.util.concurrent.*; import java.util.concurrent.*;

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>. * along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/ */
package bisq.daemon.app; package bisq.core.app;
import java.io.*; import java.io.*;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;

View file

@ -19,6 +19,7 @@ package bisq.core.app;
import bisq.core.api.AccountServiceListener; import bisq.core.api.AccountServiceListener;
import bisq.core.api.CoreAccountService; import bisq.core.api.CoreAccountService;
import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
@ -48,7 +49,11 @@ import bisq.common.util.Utilities;
import com.google.inject.Guice; import com.google.inject.Guice;
import com.google.inject.Injector; import com.google.inject.Injector;
import java.io.Console;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -60,6 +65,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
public static final int EXIT_SUCCESS = 0; public static final int EXIT_SUCCESS = 0;
public static final int EXIT_FAILURE = 1; public static final int EXIT_FAILURE = 1;
public static final int EXIT_RESTART = 2;
private final String fullName; private final String fullName;
private final String scriptName; private final String scriptName;
@ -72,6 +78,9 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
protected Config config; protected Config config;
private boolean isShutdownInProgress; private boolean isShutdownInProgress;
private boolean isReadOnly; private boolean isReadOnly;
private Thread keepRunningThread;
private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS);
private Runnable shutdownCompletedHandler;
public HavenoExecutable(String fullName, String scriptName, String appName, String version) { public HavenoExecutable(String fullName, String scriptName, String appName, String version) {
this.fullName = fullName; this.fullName = fullName;
@ -80,7 +89,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
this.version = version; this.version = version;
} }
public void execute(String[] args) { public int execute(String[] args) {
try { try {
config = new Config(appName, Utilities.getUserDataDir(), args); config = new Config(appName, Utilities.getUserDataDir(), args);
if (config.helpRequested) { if (config.helpRequested) {
@ -98,14 +107,14 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
System.exit(EXIT_FAILURE); System.exit(EXIT_FAILURE);
} }
doExecute(); return doExecute();
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// First synchronous execution tasks // First synchronous execution tasks
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
protected void doExecute() { protected int doExecute() {
CommonSetup.setup(config, this); CommonSetup.setup(config, this);
CoreSetup.setup(config); CoreSetup.setup(config);
@ -113,6 +122,8 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
// If application is JavaFX application we need to wait until it is initialized // If application is JavaFX application we need to wait until it is initialized
launchApplication(); launchApplication();
return EXIT_SUCCESS;
} }
protected abstract void configUserThread(); protected abstract void configUserThread();
@ -149,8 +160,8 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
// Application needs to restart on delete and restore of account. // Application needs to restart on delete and restore of account.
accountService.addListener(new AccountServiceListener() { accountService.addListener(new AccountServiceListener() {
@Override public void onAccountDeleted(Runnable onShutdown) { shutDownNoPersist(onShutdown); } @Override public void onAccountDeleted(Runnable onShutdown) { shutDownNoPersist(onShutdown, true); }
@Override public void onAccountRestored(Runnable onShutdown) { shutDownNoPersist(onShutdown); } @Override public void onAccountRestored(Runnable onShutdown) { shutDownNoPersist(onShutdown, true); }
}); });
// Attempt to login, subclasses should implement interactive login and or rpc login. // Attempt to login, subclasses should implement interactive login and or rpc login.
@ -165,14 +176,23 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
/** /**
* Do not persist when shutting down after account restore and restarts since * Do not persist when shutting down after account restore and restarts since
* that causes the current persistables to overwrite the restored or deleted state. * that causes the current persistables to overwrite the restored or deleted state.
*
* If restart is specified, initiates an in-process asynchronous restart of the
* application by interrupting the keepRunningThread.
*/ */
protected void shutDownNoPersist(Runnable onShutdown) { protected void shutDownNoPersist(Runnable onShutdown, boolean restart) {
this.isReadOnly = true; this.isReadOnly = true;
if (restart) {
shutdownCompletedHandler = onShutdown;
keepRunningResult.set(EXIT_RESTART);
keepRunningThread.interrupt();
} else {
gracefulShutDown(() -> { gracefulShutDown(() -> {
log.info("Shutdown without persisting"); log.info("Shutdown without persisting");
if (onShutdown != null) onShutdown.run(); if (onShutdown != null) onShutdown.run();
}); });
} }
}
/** /**
* Attempt to login. TODO: supply a password in config or args * Attempt to login. TODO: supply a password in config or args
@ -258,16 +278,32 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
// GracefulShutDownHandler implementation // GracefulShutDownHandler implementation
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// This might need to be overwritten in case the application is not using all modules
@Override @Override
public void gracefulShutDown(ResultHandler resultHandler) { public void gracefulShutDown(ResultHandler resultHandler) {
gracefulShutDown(resultHandler, true);
}
// This might need to be overwritten in case the application is not using all modules
@Override
public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) {
log.info("Start graceful shutDown"); log.info("Start graceful shutDown");
if (isShutdownInProgress) { if (isShutdownInProgress) {
log.info("Ignoring call to gracefulShutDown, already in progress");
return; return;
} }
isShutdownInProgress = true; isShutdownInProgress = true;
ResultHandler resultHandler;
if (shutdownCompletedHandler != null) {
resultHandler = () -> {
shutdownCompletedHandler.run();
onShutdown.handleResult();
};
} else {
resultHandler = onShutdown;
}
if (injector == null) { if (injector == null) {
log.info("Shut down called before injector was created"); log.info("Shut down called before injector was created");
resultHandler.handleResult(); resultHandler.handleResult();
@ -281,7 +317,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
injector.getInstance(XmrTxProofService.class).shutDown(); injector.getInstance(XmrTxProofService.class).shutDown();
injector.getInstance(AvoidStandbyModeService.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown();
injector.getInstance(TradeManager.class).shutDown(); injector.getInstance(TradeManager.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown(); // TODO: why not shut down BtcWalletService, etc? shutdown CoreMoneroConnectionsService injector.getInstance(XmrWalletService.class).shutDown(!isReadOnly); // TODO: why not shut down BtcWalletService, etc? shutdown CoreMoneroConnectionsService
log.info("OpenOfferManager shutdown started"); log.info("OpenOfferManager shutdown started");
injector.getInstance(OpenOfferManager.class).shutDown(() -> { injector.getInstance(OpenOfferManager.class).shutDown(() -> {
log.info("OpenOfferManager shutdown completed"); log.info("OpenOfferManager shutdown completed");
@ -296,35 +332,30 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
injector.getInstance(P2PService.class).shutDown(() -> { injector.getInstance(P2PService.class).shutDown(() -> {
log.info("P2PService shutdown completed"); log.info("P2PService shutdown completed");
module.close(injector); module.close(injector);
completeShutdown(resultHandler, EXIT_SUCCESS); completeShutdown(resultHandler, EXIT_SUCCESS, systemExit);
}); });
}); });
walletsSetup.shutDown(); walletsSetup.shutDown();
}); });
// Wait max 20 sec.
UserThread.runAfter(() -> {
log.warn("Graceful shut down not completed in 20 sec. We trigger our timeout handler.");
completeShutdown(resultHandler, EXIT_SUCCESS);
}, 20);
} catch (Throwable t) { } catch (Throwable t) {
log.error("App shutdown failed with exception {}", t.toString()); log.error("App shutdown failed with exception {}", t.toString());
t.printStackTrace(); t.printStackTrace();
completeShutdown(resultHandler, EXIT_FAILURE); completeShutdown(resultHandler, EXIT_FAILURE, systemExit);
} }
} }
private void completeShutdown(ResultHandler resultHandler, int exitCode) { private void completeShutdown(ResultHandler resultHandler, int exitCode, boolean systemExit) {
if (!isReadOnly) { if (!isReadOnly) {
// If user tried to downgrade we do not write the persistable data to avoid data corruption // If user tried to downgrade we do not write the persistable data to avoid data corruption
PersistenceManager.flushAllDataToDiskAtShutdown(() -> { PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
log.info("Graceful shutdown flushed persistence. Exiting now."); log.info("Graceful shutdown flushed persistence. Exiting now.");
resultHandler.handleResult(); resultHandler.handleResult();
if (systemExit)
UserThread.runAfter(() -> System.exit(exitCode), 1); UserThread.runAfter(() -> System.exit(exitCode), 1);
}); });
} else { } else {
resultHandler.handleResult(); resultHandler.handleResult();
if (systemExit)
UserThread.runAfter(() -> System.exit(exitCode), 1); UserThread.runAfter(() -> System.exit(exitCode), 1);
} }
} }
@ -341,4 +372,46 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
if (doShutDown) if (doShutDown)
gracefulShutDown(() -> log.info("gracefulShutDown complete")); gracefulShutDown(() -> log.info("gracefulShutDown complete"));
} }
/**
* Runs until a command interrupts the application and returns the desired command behavior.
* @return EXIT_SUCCESS to initiate a shutdown, EXIT_RESTART to initiate an in process restart.
*/
protected int keepRunning() {
keepRunningThread = new Thread(() -> {
ConsoleInput reader = new ConsoleInput(Integer.MAX_VALUE, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
while (true) {
Console console = System.console();
try {
if (console == null) {
Thread.sleep(Long.MAX_VALUE);
} else {
var cmd = reader.readLine();
if ("exit".equals(cmd)) {
keepRunningResult.set(EXIT_SUCCESS);
break;
} else if ("restart".equals(cmd)) {
keepRunningResult.set(EXIT_RESTART);
break;
} else if ("help".equals(cmd)) {
System.out.println("Commands: restart, exit, help");
} else {
System.out.println("Unknown command, use: restart, exit, help");
}
}
} catch (InterruptedException e) {
break;
}
}
});
keepRunningThread.start();
try {
keepRunningThread.join();
} catch (InterruptedException ie) {
System.out.println(ie);
}
return keepRunningResult.get();
}
} }

View file

@ -47,10 +47,10 @@ public class HavenoHeadlessAppMain extends HavenoExecutable {
} }
@Override @Override
protected void doExecute() { protected int doExecute() {
super.doExecute(); super.doExecute();
keepRunning(); return keepRunning();
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -114,14 +114,4 @@ public class HavenoHeadlessAppMain extends HavenoExecutable {
// In headless mode we don't have an async behaviour so we trigger the setup by calling onApplicationStarted // In headless mode we don't have an async behaviour so we trigger the setup by calling onApplicationStarted
onApplicationStarted(); onApplicationStarted();
} }
// TODO: implement interactive console which allows user to input commands; login, logoff, exit
private void keepRunning() {
while (true) {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException ignore) {
}
}
}
} }

View file

@ -97,7 +97,7 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
}); });
}); });
injector.getInstance(WalletsSetup.class).shutDown(); injector.getInstance(WalletsSetup.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown(); // TODO (woodser): this is not actually called, perhaps because WalletsSetup.class completes too quick so its listener calls System.exit(0) injector.getInstance(XmrWalletService.class).shutDown(true); // TODO (woodser): this is not actually called, perhaps because WalletsSetup.class completes too quick so its listener calls System.exit(0)
injector.getInstance(BtcWalletService.class).shutDown(); injector.getInstance(BtcWalletService.class).shutDown();
})); }));
// we wait max 5 sec. // we wait max 5 sec.
@ -187,16 +187,6 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
}, TimeUnit.HOURS.toSeconds(2)); }, TimeUnit.HOURS.toSeconds(2));
} }
@SuppressWarnings("InfiniteLoopStatement")
protected void keepRunning() {
while (true) {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException ignore) {
}
}
}
protected void checkMemory(Config config, GracefulShutDownHandler gracefulShutDownHandler) { protected void checkMemory(Config config, GracefulShutDownHandler gracefulShutDownHandler) {
int maxMemory = config.maxMemory; int maxMemory = config.maxMemory;
UserThread.runPeriodically(() -> { UserThread.runPeriodically(() -> {

View file

@ -113,13 +113,15 @@ public class MoneroWalletRpcManager {
public void stopInstance(MoneroWalletRpc walletRpc) { public void stopInstance(MoneroWalletRpc walletRpc) {
// unregister port // unregister port
int port = -1;
synchronized (registeredPorts) { synchronized (registeredPorts) {
boolean found = false; boolean found = false;
for (Map.Entry<Integer, MoneroWalletRpc> entry : registeredPorts.entrySet()) { for (Map.Entry<Integer, MoneroWalletRpc> entry : registeredPorts.entrySet()) {
if (walletRpc == entry.getValue()) { if (walletRpc == entry.getValue()) {
found = true; found = true;
try { try {
unregisterPort(entry.getKey()); port = entry.getKey();
unregisterPort(port);
} catch (Exception e) { } catch (Exception e) {
throw new MoneroError(e); throw new MoneroError(e);
} }
@ -130,6 +132,8 @@ public class MoneroWalletRpcManager {
} }
// stop process // stop process
String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid());
log.info("Stopping MoneroWalletRpc port: {} pid: {}", port, pid);
walletRpc.stopProcess(); walletRpc.stopProcess();
} }

View file

@ -252,7 +252,6 @@ public class WalletConfig extends AbstractIdleService {
@Override @Override
protected void shutDown() throws Exception { protected void shutDown() throws Exception {
} }
public NetworkParameters params() { public NetworkParameters params() {

View file

@ -146,7 +146,7 @@ public class XmrWalletService {
@Override @Override
public void onAccountClosed() { public void onAccountClosed() {
log.info(getClass() + ".accountService.onAccountClosed()"); log.info(getClass() + ".accountService.onAccountClosed()");
closeAllWallets(); closeAllWallets(true);
} }
@Override @Override
@ -527,9 +527,9 @@ public class XmrWalletService {
} }
} }
public void shutDown() { public void shutDown(boolean save) {
this.isShutDown = true; this.isShutDown = true;
closeAllWallets(); closeAllWallets(save);
} }
// ------------------------------ PRIVATE HELPERS ------------------------- // ------------------------------ PRIVATE HELPERS -------------------------
@ -746,7 +746,7 @@ public class XmrWalletService {
if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
} }
private void closeAllWallets() { private void closeAllWallets(boolean save) {
// collect wallets to shutdown // collect wallets to shutdown
List<MoneroWallet> openWallets = new ArrayList<MoneroWallet>(); List<MoneroWallet> openWallets = new ArrayList<MoneroWallet>();

View file

@ -17,6 +17,7 @@
package bisq.daemon.app; package bisq.daemon.app;
import bisq.core.app.ConsoleInput;
import bisq.core.app.HavenoHeadlessAppMain; import bisq.core.app.HavenoHeadlessAppMain;
import bisq.core.app.HavenoSetup; import bisq.core.app.HavenoSetup;
import bisq.core.api.AccountServiceListener; import bisq.core.api.AccountServiceListener;
@ -26,6 +27,7 @@ import bisq.common.UserThread;
import bisq.common.app.AppModule; import bisq.common.app.AppModule;
import bisq.common.crypto.IncorrectPasswordException; import bisq.common.crypto.IncorrectPasswordException;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
import bisq.common.persistence.PersistenceManager;
import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.ThreadFactoryBuilder;
@ -34,6 +36,7 @@ import java.util.concurrent.CancellationException;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -45,7 +48,28 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
private GrpcServer grpcServer; private GrpcServer grpcServer;
public static void main(String[] args) { public static void main(String[] args) {
new HavenoDaemonMain().execute(args); var keepRunning = true;
while (keepRunning) {
keepRunning = false;
var daemon = new HavenoDaemonMain();
var ret = daemon.execute(args);
if (ret == EXIT_SUCCESS) {
UserThread.execute(() -> daemon.gracefulShutDown(() -> {}));
} else if (ret == EXIT_RESTART) {
AtomicBoolean shuttingDown = new AtomicBoolean(true);
UserThread.execute(() -> daemon.gracefulShutDown(() -> shuttingDown.set(false), false));
keepRunning = true;
// wait for graceful shutdown
try {
while (shuttingDown.get()) {
Thread.sleep(1000);
}
PersistenceManager.reset();
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
}
}
} }
///////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
@ -106,8 +130,8 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
} }
@Override @Override
public void gracefulShutDown(ResultHandler resultHandler) { public void gracefulShutDown(ResultHandler resultHandler, boolean exit) {
super.gracefulShutDown(resultHandler); super.gracefulShutDown(resultHandler, exit);
if (grpcServer != null) grpcServer.shutdown(); // could be null if application attempted to shutdown early if (grpcServer != null) grpcServer.shutdown(); // could be null if application attempted to shutdown early
} }
@ -120,7 +144,6 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
// Start rpc server in case login is coming in from rpc // Start rpc server in case login is coming in from rpc
grpcServer = injector.getInstance(GrpcServer.class); grpcServer = injector.getInstance(GrpcServer.class);
grpcServer.start();
if (!opened) { if (!opened) {
// Nonblocking, we need to stop if the login occurred through rpc. // Nonblocking, we need to stop if the login occurred through rpc.
@ -129,7 +152,6 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
Thread t = new Thread(() -> { Thread t = new Thread(() -> {
interactiveLogin(reader); interactiveLogin(reader);
}); });
t.start();
// Handle asynchronous account opens. // Handle asynchronous account opens.
// Will need to also close and reopen account. // Will need to also close and reopen account.
@ -143,10 +165,14 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
}; };
accountService.addListener(accountListener); accountService.addListener(accountListener);
// start server after the listener is registered
grpcServer.start();
try { try {
// Wait until interactive login or rpc. Check one more time if account is open to close race condition. // Wait until interactive login or rpc. Check one more time if account is open to close race condition.
if (!accountService.isAccountOpen()) { if (!accountService.isAccountOpen()) {
log.info("Interactive login required"); log.info("Interactive login required");
t.start();
t.join(); t.join();
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
@ -155,6 +181,8 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
accountService.removeListener(accountListener); accountService.removeListener(accountListener);
opened = accountService.isAccountOpen(); opened = accountService.isAccountOpen();
} else {
grpcServer.start();
} }
return opened; return opened;

View file

@ -60,12 +60,12 @@ public class SeedNodeMain extends ExecutableForAppWithP2p {
} }
@Override @Override
protected void doExecute() { protected int doExecute() {
super.doExecute(); super.doExecute();
checkMemory(config, this); checkMemory(config, this);
keepRunning(); return keepRunning();
} }
@Override @Override

View file

@ -40,12 +40,12 @@ public class StatisticsMain extends ExecutableForAppWithP2p {
} }
@Override @Override
protected void doExecute() { protected int doExecute() {
super.doExecute(); super.doExecute();
checkMemory(config, this); checkMemory(config, this);
keepRunning(); return keepRunning();
} }
@Override @Override