fix(daemon): never restart, delete or swap under a node that hasn't exited
Every operation that stops the node (restart, rescan, repair, delete chain data, reinstall, bootstrap, wallet switch/adopt/restore/rebuild, wizard restart, encrypt, decrypt) now: - refuses up front while the node is rebuilding witnesses or has not answered for 30s, with a "try again when it's done" warning, and sends no stop; - goes ahead only once the node's PROCESS has exited, not its RPC port. A stop that doesn't finish within 120s cancels the operation (warning; a cancelled rescan/repair disarms its flags); nothing is started over the old node, chain data isn't deleted, wallet.dat isn't swapped. Specifics: - stopDaemonForWalletSwitch() no longer reports "stopped" when the app is quitting (adopt could swap wallet.dat under a live node). - Decrypt waited a fixed 3s after "stop" before renaming wallet.dat; it now waits for the process to exit, and scrubs the key export if not. - Bootstrap waits for the process before cleanChainData and stops the node through the stop routine instead of the shared RPC client. - StopScope::Connected lets switch/reinstall/decrypt stop an adopted or external node through the same never-kill routine. - A bounded stop that can't reach the node (a foreign node refusing our credentials) gives up after 5s instead of 120s. - macOS: isDaemonProcessRunning() checks processes (libproc), not the RPC port. - rpcBusyWhileAlive() also reads the busy gate: since the gate, the first timeout usually lands on another lane and the 30s clock never started (also weakened the quit prompt for a silent rebuild). - Notifications render during the first-run wizard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -100,6 +100,8 @@
|
|||||||
"appx_no_bundled_daemon_to_install": "Dieser Build enthält keinen mitgelieferten Daemon zur Installation",
|
"appx_no_bundled_daemon_to_install": "Dieser Build enthält keinen mitgelieferten Daemon zur Installation",
|
||||||
"appx_no_embedded_daemon_to_install": "Dieser Build enthält keinen eingebetteten Daemon zur Installation",
|
"appx_no_embedded_daemon_to_install": "Dieser Build enthält keinen eingebetteten Daemon zur Installation",
|
||||||
"appx_node_busy_restarting": "Der Node ist mit dem Neustart beschäftigt — versuchen Sie es gleich erneut.",
|
"appx_node_busy_restarting": "Der Node ist mit dem Neustart beschäftigt — versuchen Sie es gleich erneut.",
|
||||||
|
"appx_node_busy_try_later": "Der Node ist mit einer langen Aufgabe beschäftigt (etwa dem Neuaufbau der Note-Witnesses). Versuchen Sie es erneut, wenn sie abgeschlossen ist; ein Stoppen jetzt könnte diese Arbeit von vorn beginnen lassen.",
|
||||||
|
"appx_node_not_stopped": "Der Node wurde nicht rechtzeitig beendet, daher wurde nichts geändert. Versuchen Sie es in ein paar Minuten erneut.",
|
||||||
"appx_not_next_word": " — das ist nicht das nächste Wort",
|
"appx_not_next_word": " — das ist nicht das nächste Wort",
|
||||||
"appx_payment_request_loaded": "Zahlungsanforderung geladen",
|
"appx_payment_request_loaded": "Zahlungsanforderung geladen",
|
||||||
"appx_pool_miner_connected_and_hashing": "Pool-Miner verbunden und aktiv.",
|
"appx_pool_miner_connected_and_hashing": "Pool-Miner verbunden und aktiv.",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"appx_no_bundled_daemon_to_install": "Esta versión no incluye un daemon para instalar",
|
"appx_no_bundled_daemon_to_install": "Esta versión no incluye un daemon para instalar",
|
||||||
"appx_no_embedded_daemon_to_install": "Esta versión no incluye un daemon integrado para instalar",
|
"appx_no_embedded_daemon_to_install": "Esta versión no incluye un daemon integrado para instalar",
|
||||||
"appx_node_busy_restarting": "El nodo está ocupado reiniciándose; inténtalo de nuevo en un momento.",
|
"appx_node_busy_restarting": "El nodo está ocupado reiniciándose; inténtalo de nuevo en un momento.",
|
||||||
|
"appx_node_busy_try_later": "El nodo está ocupado terminando una tarea larga (como reconstruir los testigos de notas). Vuelve a intentarlo cuando termine; detenerlo ahora podría hacer que empiece ese trabajo de nuevo.",
|
||||||
|
"appx_node_not_stopped": "El nodo no se detuvo a tiempo, así que no se cambió nada. Vuelve a intentarlo en unos minutos.",
|
||||||
"appx_not_next_word": " — esa no es la siguiente palabra",
|
"appx_not_next_word": " — esa no es la siguiente palabra",
|
||||||
"appx_payment_request_loaded": "Solicitud de pago cargada",
|
"appx_payment_request_loaded": "Solicitud de pago cargada",
|
||||||
"appx_pool_miner_connected_and_hashing": "Minero de pool conectado y calculando hashes.",
|
"appx_pool_miner_connected_and_hashing": "Minero de pool conectado y calculando hashes.",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"appx_no_bundled_daemon_to_install": "Cette version ne contient aucun démon intégré à installer",
|
"appx_no_bundled_daemon_to_install": "Cette version ne contient aucun démon intégré à installer",
|
||||||
"appx_no_embedded_daemon_to_install": "Cette version ne contient aucun démon embarqué à installer",
|
"appx_no_embedded_daemon_to_install": "Cette version ne contient aucun démon embarqué à installer",
|
||||||
"appx_node_busy_restarting": "Le nœud est occupé à redémarrer — réessayez dans un instant.",
|
"appx_node_busy_restarting": "Le nœud est occupé à redémarrer — réessayez dans un instant.",
|
||||||
|
"appx_node_busy_try_later": "Le nœud termine une longue tâche (comme la reconstruction des témoins de notes). Réessayez une fois qu'elle est finie ; l'arrêter maintenant pourrait lui faire recommencer ce travail.",
|
||||||
|
"appx_node_not_stopped": "Le nœud ne s'est pas arrêté à temps, rien n'a donc été modifié. Réessayez dans quelques minutes.",
|
||||||
"appx_not_next_word": " — ce n'est pas le mot suivant",
|
"appx_not_next_word": " — ce n'est pas le mot suivant",
|
||||||
"appx_payment_request_loaded": "Demande de paiement chargée",
|
"appx_payment_request_loaded": "Demande de paiement chargée",
|
||||||
"appx_pool_miner_connected_and_hashing": "Mineur de pool connecté et en cours de hachage.",
|
"appx_pool_miner_connected_and_hashing": "Mineur de pool connecté et en cours de hachage.",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"appx_no_bundled_daemon_to_install": "このビルドにはインストールできる同梱デーモンがありません",
|
"appx_no_bundled_daemon_to_install": "このビルドにはインストールできる同梱デーモンがありません",
|
||||||
"appx_no_embedded_daemon_to_install": "このビルドにはインストールできる組み込みデーモンがありません",
|
"appx_no_embedded_daemon_to_install": "このビルドにはインストールできる組み込みデーモンがありません",
|
||||||
"appx_node_busy_restarting": "ノードが再起動中でビジー状態です。少し待ってから再試行してください。",
|
"appx_node_busy_restarting": "ノードが再起動中でビジー状態です。少し待ってから再試行してください。",
|
||||||
|
"appx_node_busy_try_later": "ノードは長時間の処理(ノートウィットネスの再構築など)を実行中です。完了してからもう一度お試しください。今停止すると、その処理を最初からやり直すことがあります。",
|
||||||
|
"appx_node_not_stopped": "ノードが時間内に停止しなかったため、何も変更されませんでした。数分後にもう一度お試しください。",
|
||||||
"appx_not_next_word": " — 次の単語ではありません",
|
"appx_not_next_word": " — 次の単語ではありません",
|
||||||
"appx_payment_request_loaded": "支払いリクエストを読み込みました",
|
"appx_payment_request_loaded": "支払いリクエストを読み込みました",
|
||||||
"appx_pool_miner_connected_and_hashing": "プールマイナーが接続し、ハッシュ計算中です。",
|
"appx_pool_miner_connected_and_hashing": "プールマイナーが接続し、ハッシュ計算中です。",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"appx_no_bundled_daemon_to_install": "이 빌드에는 설치할 번들 데몬이 없습니다",
|
"appx_no_bundled_daemon_to_install": "이 빌드에는 설치할 번들 데몬이 없습니다",
|
||||||
"appx_no_embedded_daemon_to_install": "이 빌드에는 설치할 내장 데몬이 없습니다",
|
"appx_no_embedded_daemon_to_install": "이 빌드에는 설치할 내장 데몬이 없습니다",
|
||||||
"appx_node_busy_restarting": "노드가 재시작 중입니다 — 잠시 후 다시 시도하세요.",
|
"appx_node_busy_restarting": "노드가 재시작 중입니다 — 잠시 후 다시 시도하세요.",
|
||||||
|
"appx_node_busy_try_later": "노드가 긴 작업(노트 위트니스 재구성 등)을 마무리하는 중입니다. 끝난 뒤 다시 시도하세요. 지금 중지하면 그 작업을 처음부터 다시 할 수 있습니다.",
|
||||||
|
"appx_node_not_stopped": "노드가 제때 중지되지 않아 아무것도 변경되지 않았습니다. 몇 분 후 다시 시도하세요.",
|
||||||
"appx_not_next_word": " — 다음 단어가 아닙니다",
|
"appx_not_next_word": " — 다음 단어가 아닙니다",
|
||||||
"appx_payment_request_loaded": "결제 요청을 불러왔습니다",
|
"appx_payment_request_loaded": "결제 요청을 불러왔습니다",
|
||||||
"appx_pool_miner_connected_and_hashing": "풀 채굴기가 연결되어 해싱 중입니다.",
|
"appx_pool_miner_connected_and_hashing": "풀 채굴기가 연결되어 해싱 중입니다.",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"appx_no_bundled_daemon_to_install": "Esta versão não tem daemon incluído para instalar",
|
"appx_no_bundled_daemon_to_install": "Esta versão não tem daemon incluído para instalar",
|
||||||
"appx_no_embedded_daemon_to_install": "Esta versão não tem daemon embutido para instalar",
|
"appx_no_embedded_daemon_to_install": "Esta versão não tem daemon embutido para instalar",
|
||||||
"appx_node_busy_restarting": "O nó está ocupado reiniciando — tente novamente em instantes.",
|
"appx_node_busy_restarting": "O nó está ocupado reiniciando — tente novamente em instantes.",
|
||||||
|
"appx_node_busy_try_later": "O nó está ocupado concluindo uma tarefa longa (como reconstruir as testemunhas de notas). Tente novamente quando terminar; pará-lo agora pode fazê-lo recomeçar esse trabalho.",
|
||||||
|
"appx_node_not_stopped": "O nó não parou a tempo, por isso nada foi alterado. Tente novamente em alguns minutos.",
|
||||||
"appx_not_next_word": " — essa não é a próxima palavra",
|
"appx_not_next_word": " — essa não é a próxima palavra",
|
||||||
"appx_payment_request_loaded": "Solicitação de pagamento carregada",
|
"appx_payment_request_loaded": "Solicitação de pagamento carregada",
|
||||||
"appx_pool_miner_connected_and_hashing": "Minerador de pool conectado e processando hashes.",
|
"appx_pool_miner_connected_and_hashing": "Minerador de pool conectado e processando hashes.",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"appx_no_bundled_daemon_to_install": "В этой сборке нет встроенного демона для установки",
|
"appx_no_bundled_daemon_to_install": "В этой сборке нет встроенного демона для установки",
|
||||||
"appx_no_embedded_daemon_to_install": "В этой сборке нет встроенного демона для установки",
|
"appx_no_embedded_daemon_to_install": "В этой сборке нет встроенного демона для установки",
|
||||||
"appx_node_busy_restarting": "Узел занят перезапуском — попробуйте ещё раз через мгновение.",
|
"appx_node_busy_restarting": "Узел занят перезапуском — попробуйте ещё раз через мгновение.",
|
||||||
|
"appx_node_busy_try_later": "Узел занят долгой задачей (например, перестройкой свидетелей заметок). Повторите попытку, когда она завершится; если остановить его сейчас, эту работу, возможно, придётся начать заново.",
|
||||||
|
"appx_node_not_stopped": "Узел не остановился вовремя, поэтому ничего не изменено. Повторите попытку через несколько минут.",
|
||||||
"appx_not_next_word": " — это не следующее слово",
|
"appx_not_next_word": " — это не следующее слово",
|
||||||
"appx_payment_request_loaded": "Запрос на оплату загружен",
|
"appx_payment_request_loaded": "Запрос на оплату загружен",
|
||||||
"appx_pool_miner_connected_and_hashing": "Пул-майнер подключён и вычисляет хеши.",
|
"appx_pool_miner_connected_and_hashing": "Пул-майнер подключён и вычисляет хеши.",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"appx_no_bundled_daemon_to_install": "此版本没有可安装的捆绑守护进程",
|
"appx_no_bundled_daemon_to_install": "此版本没有可安装的捆绑守护进程",
|
||||||
"appx_no_embedded_daemon_to_install": "此版本没有可安装的内嵌守护进程",
|
"appx_no_embedded_daemon_to_install": "此版本没有可安装的内嵌守护进程",
|
||||||
"appx_node_busy_restarting": "节点正忙于重启——请稍后再试。",
|
"appx_node_busy_restarting": "节点正忙于重启——请稍后再试。",
|
||||||
|
"appx_node_busy_try_later": "节点正忙于完成一项耗时任务(例如重建票据见证)。请在完成后重试;现在停止它可能导致这项工作从头开始。",
|
||||||
|
"appx_node_not_stopped": "节点未能及时停止,因此未做任何更改。请几分钟后重试。",
|
||||||
"appx_not_next_word": " — 这不是下一个词",
|
"appx_not_next_word": " — 这不是下一个词",
|
||||||
"appx_payment_request_loaded": "支付请求已加载",
|
"appx_payment_request_loaded": "支付请求已加载",
|
||||||
"appx_pool_miner_connected_and_hashing": "矿池矿工已连接并正在计算哈希。",
|
"appx_pool_miner_connected_and_hashing": "矿池矿工已连接并正在计算哈希。",
|
||||||
|
|||||||
@@ -25,6 +25,26 @@ def load_english_keys():
|
|||||||
|
|
||||||
# All translations for the 158 missing keys
|
# All translations for the 158 missing keys
|
||||||
TRANSLATIONS = {
|
TRANSLATIONS = {
|
||||||
|
"appx_node_busy_try_later": {
|
||||||
|
"de": "Der Node ist mit einer langen Aufgabe beschäftigt (etwa dem Neuaufbau der Note-Witnesses). Versuchen Sie es erneut, wenn sie abgeschlossen ist; ein Stoppen jetzt könnte diese Arbeit von vorn beginnen lassen.",
|
||||||
|
"es": "El nodo está ocupado terminando una tarea larga (como reconstruir los testigos de notas). Vuelve a intentarlo cuando termine; detenerlo ahora podría hacer que empiece ese trabajo de nuevo.",
|
||||||
|
"fr": "Le nœud termine une longue tâche (comme la reconstruction des témoins de notes). Réessayez une fois qu'elle est finie ; l'arrêter maintenant pourrait lui faire recommencer ce travail.",
|
||||||
|
"pt": "O nó está ocupado concluindo uma tarefa longa (como reconstruir as testemunhas de notas). Tente novamente quando terminar; pará-lo agora pode fazê-lo recomeçar esse trabalho.",
|
||||||
|
"ru": "Узел занят долгой задачей (например, перестройкой свидетелей заметок). Повторите попытку, когда она завершится; если остановить его сейчас, эту работу, возможно, придётся начать заново.",
|
||||||
|
"zh": "节点正忙于完成一项耗时任务(例如重建票据见证)。请在完成后重试;现在停止它可能导致这项工作从头开始。",
|
||||||
|
"ja": "ノードは長時間の処理(ノートウィットネスの再構築など)を実行中です。完了してからもう一度お試しください。今停止すると、その処理を最初からやり直すことがあります。",
|
||||||
|
"ko": "노드가 긴 작업(노트 위트니스 재구성 등)을 마무리하는 중입니다. 끝난 뒤 다시 시도하세요. 지금 중지하면 그 작업을 처음부터 다시 할 수 있습니다.",
|
||||||
|
},
|
||||||
|
"appx_node_not_stopped": {
|
||||||
|
"de": "Der Node wurde nicht rechtzeitig beendet, daher wurde nichts geändert. Versuchen Sie es in ein paar Minuten erneut.",
|
||||||
|
"es": "El nodo no se detuvo a tiempo, así que no se cambió nada. Vuelve a intentarlo en unos minutos.",
|
||||||
|
"fr": "Le nœud ne s'est pas arrêté à temps, rien n'a donc été modifié. Réessayez dans quelques minutes.",
|
||||||
|
"pt": "O nó não parou a tempo, por isso nada foi alterado. Tente novamente em alguns minutos.",
|
||||||
|
"ru": "Узел не остановился вовремя, поэтому ничего не изменено. Повторите попытку через несколько минут.",
|
||||||
|
"zh": "节点未能及时停止,因此未做任何更改。请几分钟后重试。",
|
||||||
|
"ja": "ノードが時間内に停止しなかったため、何も変更されませんでした。数分後にもう一度お試しください。",
|
||||||
|
"ko": "노드가 제때 중지되지 않아 아무것도 변경되지 않았습니다. 몇 분 후 다시 시도하세요.",
|
||||||
|
},
|
||||||
# --- Daemon shutdown hardening: stopping the node on quit (never kill unasked) ---
|
# --- Daemon shutdown hardening: stopping the node on quit (never kill unasked) ---
|
||||||
"shutdown_leave_running": {
|
"shutdown_leave_running": {
|
||||||
"de": "Wallet schließen – Node weiterlaufen lassen", "es": "Cerrar la cartera y dejar el nodo en marcha",
|
"de": "Wallet schließen – Node weiterlaufen lassen", "es": "Cerrar la cartera y dejar el nodo en marcha",
|
||||||
|
|||||||
+93
-75
@@ -315,7 +315,7 @@ class AppDaemonLifecycleRuntime final : public daemon::DaemonController::Lifecyc
|
|||||||
public:
|
public:
|
||||||
explicit AppDaemonLifecycleRuntime(App& app) : app_(app) {}
|
explicit AppDaemonLifecycleRuntime(App& app) : app_(app) {}
|
||||||
|
|
||||||
void stopDaemonWithPolicy() override { app_.stopEmbeddedDaemon(); }
|
bool stopDaemonWithPolicy() override { return app_.stopEmbeddedDaemon() == daemon::StopOutcome::Exited; }
|
||||||
bool startDaemon() override { return app_.startEmbeddedDaemon(); }
|
bool startDaemon() override { return app_.startEmbeddedDaemon(); }
|
||||||
void resetOutputOffset() override
|
void resetOutputOffset() override
|
||||||
{
|
{
|
||||||
@@ -323,13 +323,13 @@ public:
|
|||||||
app_.resetDaemonBusy();
|
app_.resetDaemonBusy();
|
||||||
}
|
}
|
||||||
|
|
||||||
void requestRpcStopAndDisconnect(const char* context, const char* reason) override
|
void beginBackgroundStop(const char* reason) override
|
||||||
{
|
{
|
||||||
if (app_.rpc_ && app_.rpc_->isConnected()) {
|
if (app_.rpc_ && app_.rpc_->isConnected()) app_.rpc_->disconnect();
|
||||||
app_.sendStopCommandSafely(*app_.rpc_, context);
|
|
||||||
app_.rpc_->disconnect();
|
|
||||||
}
|
|
||||||
app_.onDisconnected(reason ? reason : "Daemon lifecycle");
|
app_.onDisconnected(reason ? reason : "Daemon lifecycle");
|
||||||
|
App& app = app_;
|
||||||
|
app.async_tasks_.submit("background-stop-daemon",
|
||||||
|
[&app](const util::AsyncTaskManager::Token&) { app.stopEmbeddedDaemon(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
int deleteBlockchainData() override
|
int deleteBlockchainData() override
|
||||||
@@ -1614,6 +1614,7 @@ void App::render()
|
|||||||
if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) {
|
if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) {
|
||||||
renderFirstRunWizard();
|
renderFirstRunWizard();
|
||||||
renderDaemonStopConfirm(); // a quit during the wizard still gets its "keep the node running?" prompt
|
renderDaemonStopConfirm(); // a quit during the wizard still gets its "keep the node running?" prompt
|
||||||
|
ui::Notifications::instance().render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1643,6 +1644,19 @@ void App::render()
|
|||||||
}
|
}
|
||||||
if (pending_reinstall_node_busy_.exchange(false, std::memory_order_relaxed))
|
if (pending_reinstall_node_busy_.exchange(false, std::memory_order_relaxed))
|
||||||
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_node_busy"), 12.0f);
|
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_node_busy"), 12.0f);
|
||||||
|
if (const int failed = pending_stop_failed_.exchange(0, std::memory_order_relaxed)) {
|
||||||
|
if ((failed & kStopFailedUndoRescan) && daemon_controller_) {
|
||||||
|
// The rescan/repair never started: disarm its flags so a later start doesn't run it unasked.
|
||||||
|
daemon_controller_->setRescanOnNextStart(false);
|
||||||
|
daemon_controller_->setZapOnNextStart(false);
|
||||||
|
state_.sync.rescanning = false;
|
||||||
|
state_.sync.rescan_status.clear();
|
||||||
|
rescan_confirmed_active_ = false;
|
||||||
|
user_initiated_rescan_ = false;
|
||||||
|
}
|
||||||
|
if ((failed & kStopFailedNotify) && !shutting_down_)
|
||||||
|
ui::Notifications::instance().warning(TR("appx_node_not_stopped"), 12.0f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -5554,22 +5568,23 @@ void App::chooseStopAction(daemon::StopUserChoice choice)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
daemon::StopOutcome App::stopEmbeddedDaemon(StopMode mode)
|
daemon::StopOutcome App::stopEmbeddedDaemon(StopMode mode, StopScope scope)
|
||||||
{
|
{
|
||||||
if (!daemon_controller_) return daemon::StopOutcome::Exited;
|
if (scope == StopScope::Policy) {
|
||||||
|
if (!daemon_controller_) return daemon::StopOutcome::Exited;
|
||||||
auto shutdownDecision = daemon_controller_->shutdownDecision(
|
auto shutdownDecision = daemon_controller_->shutdownDecision(
|
||||||
false, settings_ && settings_->getStopExternalDaemon());
|
false, settings_ && settings_->getStopExternalDaemon());
|
||||||
if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) {
|
if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) {
|
||||||
DEBUG_LOGF("stopEmbeddedDaemon: %s — skipping\n", shutdownDecision.logReason);
|
DEBUG_LOGF("stopEmbeddedDaemon: %s — skipping\n", shutdownDecision.logReason);
|
||||||
return daemon::StopOutcome::StillRunning;
|
return daemon::StopOutcome::StillRunning;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// One stop at a time: a second caller (say the shutdown thread while a restart task is stopping the
|
// One stop at a time: a second caller (say the shutdown thread while a restart task is stopping the
|
||||||
// node) waits for the first. Background stops let go as soon as the app starts quitting, so the
|
// node) waits for the first. Background stops let go as soon as the app starts quitting, so the
|
||||||
// shutdown never waits long behind one.
|
// shutdown never waits long behind one.
|
||||||
std::lock_guard<std::mutex> single(stop_mutex_);
|
std::lock_guard<std::mutex> single(stop_mutex_);
|
||||||
daemon::EmbeddedDaemon* d = daemon_controller_->daemon();
|
daemon::EmbeddedDaemon* d = daemon_controller_ ? daemon_controller_->daemon() : nullptr;
|
||||||
// beginStop() also marks the stop as requested (an early exit is not a crash) and makes this session
|
// beginStop() also marks the stop as requested (an early exit is not a crash) and makes this session
|
||||||
// the only reaper. Only a process we launched and still hold is "owned": a lock-gated start, an
|
// the only reaper. Only a process we launched and still hold is "owned": a lock-gated start, an
|
||||||
// external node and a node we already let go of are not, and are never waited on, signalled or killed.
|
// external node and a node we already let go of are not, and are never waited on, signalled or killed.
|
||||||
@@ -5654,52 +5669,23 @@ bool App::isEmbeddedDaemonRunning() const
|
|||||||
|
|
||||||
bool App::stopDaemonForWalletSwitch()
|
bool App::stopDaemonForWalletSwitch()
|
||||||
{
|
{
|
||||||
// "Owned" means we spawned the node this session and hold a live process handle. That is the ONLY
|
// Stop whatever node we're on: ours, or the adopted/external one we're connected to. "Owned" can't be
|
||||||
// reliable ownership signal here: externalDaemonDetected() is latched only inside EmbeddedDaemon::start(),
|
// read from externalDaemonDetected() (it's latched only inside EmbeddedDaemon::start(), so a node we
|
||||||
// so when the app just direct-connects to an already-running, config-provided node (the common
|
// direct-connected to reads as ours); StopScope::Connected covers both — our process handle when we have
|
||||||
// keep-node-running case) start() is never called and that flag stays false even though we clearly did
|
// one, else a graceful "stop" with the exact credentials we connected with (only our node accepts them,
|
||||||
// not start the node. Gate on the handle instead.
|
// so a foreign dragonxd is a safe no-op). Bounded, never killed, abandoned when the app starts quitting.
|
||||||
const bool owned = daemon_controller_ && isEmbeddedDaemonRunning();
|
const daemon::StopOutcome outcome = stopEmbeddedDaemon(StopMode::Background, StopScope::Connected);
|
||||||
if (owned) {
|
// CRITICAL: the caller swaps wallet.dat or starts a replacement next, so it may go ahead only once the node
|
||||||
// We spawned it — stopEmbeddedDaemon() waits for the process to exit (up to 120s, never killing
|
// has fully EXITED. dragonxd unbinds RPC early in its shutdown but keeps wallet.dat and the datadir lock
|
||||||
// it), so on Exited the datadir lock + RPC port are released; a short port check just confirms.
|
// until the process exits, which can take minutes. A wait abandoned because the app is quitting proves
|
||||||
// Still running means the switch must not go ahead: the old node holds the wallet and the lock.
|
// nothing: report "not stopped" then too.
|
||||||
if (stopEmbeddedDaemon() != daemon::StopOutcome::Exited) return shutting_down_;
|
if (outcome != daemon::StopOutcome::Exited || shutting_down_) return false;
|
||||||
for (int i = 0; i < 200 && daemon::EmbeddedDaemon::isRpcPortInUse() && !shutting_down_; ++i)
|
auto stillUp = [] {
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
||||||
return shutting_down_ || !daemon::EmbeddedDaemon::isRpcPortInUse();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adopted OR direct-connected: no process handle, so RPC "stop" is the only lever, and
|
|
||||||
// stopEmbeddedDaemon()'s temp connection (autoDetectConfig) can't be trusted here. Send a graceful RPC
|
|
||||||
// "stop" over the exact creds we're connected with (saved_config_) — guaranteed to reach our node, and
|
|
||||||
// only OUR node accepts them, so a foreign dragonxd (different rpcpassword) is a safe no-op. switchToWallet
|
|
||||||
// disconnects rpc_ before this worker runs, so build a fresh temporary connection.
|
|
||||||
bool sent = false;
|
|
||||||
{
|
|
||||||
auto tmp = std::make_unique<rpc::RPCClient>();
|
|
||||||
if (tmp->connect(saved_config_.host, saved_config_.port, saved_config_.rpcuser,
|
|
||||||
saved_config_.rpcpassword, saved_config_.use_tls)) {
|
|
||||||
sent = sendStopCommandSafely(*tmp, "wallet-switch stop");
|
|
||||||
tmp->disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DEBUG_LOGF("[App] wallet-switch stop of unowned node (saved_config_): %s\n", sent ? "sent" : "FAILED");
|
|
||||||
|
|
||||||
// CRITICAL: wait for the node to FULLY EXIT before the caller starts the replacement — NOT just for the
|
|
||||||
// RPC port. On Windows isRpcPortInUse() is a connect() probe that reads "free" the moment the daemon
|
|
||||||
// stops accepting RPC (early in shutdown), but the process keeps the DATADIR LOCK until it exits, and a
|
|
||||||
// graceful shutdown can take 60-90s when the node's network threads block on peer timeouts. Starting a
|
|
||||||
// replacement into a still-locked datadir fails ("Cannot obtain a lock…") and wedges the switch. So gate
|
|
||||||
// on the PROCESS being gone (isDaemonProcessRunning) AND the port free. Bounded ~120s while stopping; if
|
|
||||||
// we couldn't even send the stop (foreign node / bad creds → its port never frees), only a brief grace.
|
|
||||||
const int maxTicks = sent ? 1200 : 50;
|
|
||||||
auto stillUp = []() {
|
|
||||||
return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning();
|
return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning();
|
||||||
};
|
};
|
||||||
for (int i = 0; i < maxTicks && stillUp() && !shutting_down_; ++i)
|
for (int i = 0; i < 200 && stillUp() && !shutting_down_; ++i) // up to 20s for the port to free
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
return shutting_down_ || !stillUp();
|
return !shutting_down_ && !stillUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::rescanBlockchain()
|
void App::rescanBlockchain()
|
||||||
@@ -5728,6 +5714,7 @@ void App::rescanBlockchain()
|
|||||||
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
|
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (refuseIfNodeBusy()) return;
|
||||||
|
|
||||||
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
|
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
|
||||||
ui::Notifications::instance().info(TR("appx_restarting_daemon_rescan_flag"));
|
ui::Notifications::instance().info(TR("appx_restarting_daemon_rescan_flag"));
|
||||||
@@ -5753,7 +5740,8 @@ void App::rescanBlockchain()
|
|||||||
DEBUG_LOGF("[App] Stopping daemon for rescan...\n");
|
DEBUG_LOGF("[App] Stopping daemon for rescan...\n");
|
||||||
AppDaemonLifecycleRuntime runtime(*this);
|
AppDaemonLifecycleRuntime runtime(*this);
|
||||||
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
||||||
daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
if (daemon_controller_->executeLifecycleOperation(decision, runtime, context).stopFailed)
|
||||||
|
pending_stop_failed_.fetch_or(kStopFailedNotify | kStopFailedUndoRescan);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5780,6 +5768,7 @@ void App::repairWallet()
|
|||||||
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
|
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (refuseIfNodeBusy()) return;
|
||||||
|
|
||||||
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n");
|
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n");
|
||||||
ui::Notifications::instance().info(TR("appx_restarting_daemon_zapwallettxes"));
|
ui::Notifications::instance().info(TR("appx_restarting_daemon_zapwallettxes"));
|
||||||
@@ -5804,7 +5793,8 @@ void App::repairWallet()
|
|||||||
DEBUG_LOGF("[App] Stopping daemon for wallet repair...\n");
|
DEBUG_LOGF("[App] Stopping daemon for wallet repair...\n");
|
||||||
AppDaemonLifecycleRuntime runtime(*this);
|
AppDaemonLifecycleRuntime runtime(*this);
|
||||||
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
||||||
daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
if (daemon_controller_->executeLifecycleOperation(decision, runtime, context).stopFailed)
|
||||||
|
pending_stop_failed_.fetch_or(kStopFailedNotify | kStopFailedUndoRescan);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5831,6 +5821,7 @@ void App::reinstallBundledDaemon()
|
|||||||
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_in_progress"));
|
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_in_progress"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (refuseIfNodeBusy()) return;
|
||||||
|
|
||||||
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
|
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
|
||||||
ui::Notifications::instance().info(TR("appx_installing_bundled_daemon"));
|
ui::Notifications::instance().info(TR("appx_installing_bundled_daemon"));
|
||||||
@@ -5838,14 +5829,9 @@ void App::reinstallBundledDaemon()
|
|||||||
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||||
AppDaemonLifecycleRuntime runtime(*this);
|
AppDaemonLifecycleRuntime runtime(*this);
|
||||||
daemon::AsyncLifecycleTaskContext ctx(token, shutting_down_);
|
daemon::AsyncLifecycleTaskContext ctx(token, shutting_down_);
|
||||||
// Stop the running node so its binary is free. stopDaemonWithPolicy() covers a wallet-managed
|
// Stop the running node so its binary is free: ours, or the external/leftover dragonxd we're attached
|
||||||
// daemon; if we're instead attached to an external/leftover dragonxd (daemon_controller_ is
|
// to (it would keep the RPC port and the fresh daemon couldn't start). Bounded, never killed.
|
||||||
// null, so stopEmbeddedDaemon() returns immediately), send an explicit RPC stop so that node
|
stopEmbeddedDaemon(StopMode::Background, StopScope::Connected);
|
||||||
// exits too — otherwise it keeps the RPC port and the fresh daemon can't start.
|
|
||||||
if (!daemon_controller_ && rpc_ && rpc_->isConnected()) {
|
|
||||||
sendStopCommandSafely(*rpc_, "reinstall-daemon");
|
|
||||||
}
|
|
||||||
runtime.stopDaemonWithPolicy();
|
|
||||||
// Wait (bounded, ~120s) for the old node's PROCESS to exit, not just its RPC port: dragonxd unbinds
|
// Wait (bounded, ~120s) for the old node's PROCESS to exit, not just its RPC port: dragonxd unbinds
|
||||||
// RPC first and can take a minute or more to flush and close its wallet. Until it exits its exe is
|
// RPC first and can take a minute or more to flush and close its wallet. Until it exits its exe is
|
||||||
// locked on Windows (the overwrite would fail) and it holds the datadir lock (a restart would fail).
|
// locked on Windows (the overwrite would fail) and it holds the datadir lock (a restart would fail).
|
||||||
@@ -5892,6 +5878,7 @@ void App::deleteBlockchainData()
|
|||||||
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
|
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (refuseIfNodeBusy()) return;
|
||||||
|
|
||||||
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
|
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
|
||||||
ui::Notifications::instance().info(TR("appx_stopping_daemon_deleting_blockchain"));
|
ui::Notifications::instance().info(TR("appx_stopping_daemon_deleting_blockchain"));
|
||||||
@@ -5902,6 +5889,11 @@ void App::deleteBlockchainData()
|
|||||||
AppDaemonLifecycleRuntime runtime(*this);
|
AppDaemonLifecycleRuntime runtime(*this);
|
||||||
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
||||||
auto result = daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
auto result = daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
||||||
|
if (result.stopFailed) {
|
||||||
|
DEBUG_LOGF("[App] Blockchain data NOT deleted: the node did not exit\n");
|
||||||
|
pending_stop_failed_.fetch_or(kStopFailedNotify);
|
||||||
|
return;
|
||||||
|
}
|
||||||
DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", result.deletedItems);
|
DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", result.deletedItems);
|
||||||
// Hand the count to the main thread for a completion toast (Notifications isn't thread-safe).
|
// Hand the count to the main thread for a completion toast (Notifications isn't thread-safe).
|
||||||
pending_delete_result_.store(result.deletedItems, std::memory_order_relaxed);
|
pending_delete_result_.store(result.deletedItems, std::memory_order_relaxed);
|
||||||
@@ -6196,8 +6188,12 @@ void App::noteDaemonRpcResult(bool answered, rpc::RpcFailureKind kind)
|
|||||||
|
|
||||||
bool App::rpcBusyWhileAlive() const
|
bool App::rpcBusyWhileAlive() const
|
||||||
{
|
{
|
||||||
if (rpc_busy_since_ == std::chrono::steady_clock::time_point{}) return false;
|
// Two clocks: the connect/core-refresh results, and the busy gate. The gate hears every lane (since S6 the
|
||||||
if (std::chrono::steady_clock::now() - rpc_busy_since_ < std::chrono::seconds(30)) return false;
|
// first timeout often lands on another lane, after which the core refresh is only ever Deferred).
|
||||||
|
const bool resultsBusy = rpc_busy_since_ != std::chrono::steady_clock::time_point{} &&
|
||||||
|
std::chrono::steady_clock::now() - rpc_busy_since_ >= std::chrono::seconds(30);
|
||||||
|
const bool gateBusy = rpc_busy_gate_ && rpc_busy_gate_->busySeconds() >= 30.0;
|
||||||
|
if (!resultsBusy && !gateBusy) return false;
|
||||||
return (daemon_controller_ && daemon_controller_->isRunning()) || daemon::EmbeddedDaemon::isRpcPortInUse();
|
return (daemon_controller_ && daemon_controller_->isRunning()) || daemon::EmbeddedDaemon::isRpcPortInUse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6210,12 +6206,27 @@ bool App::shouldConfirmDaemonStop() const
|
|||||||
settings_ && settings_->getKeepDaemonRunning(),
|
settings_ && settings_->getKeepDaemonRunning(),
|
||||||
settings_ && settings_->getStopExternalDaemon());
|
settings_ && settings_->getStopExternalDaemon());
|
||||||
const bool stops = decision.action == daemon::DaemonController::ShutdownAction::StopDaemon;
|
const bool stops = decision.action == daemon::DaemonController::ShutdownAction::StopDaemon;
|
||||||
// The tracker reads OUR node's output; once that process is gone its last state means nothing. No
|
// No daemon version suppresses this: v1.3.0 does not checkpoint a witness rebuild, and nothing survives
|
||||||
// daemon version suppresses this: v1.3.0 does not checkpoint a witness rebuild, and nothing survives
|
|
||||||
// a kill.
|
// a kill.
|
||||||
|
return stops && nodeBusyForStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool App::nodeBusyForStop() const
|
||||||
|
{
|
||||||
|
// The tracker reads OUR node's output; once that process is gone its last state means nothing. An
|
||||||
|
// external node's busyness shows only as RPC that has stopped answering.
|
||||||
daemon::DaemonBusyTracker ours;
|
daemon::DaemonBusyTracker ours;
|
||||||
if (daemon_controller_->isRunning()) ours = daemon_busy_;
|
if (daemon_controller_ && daemon_controller_->isRunning()) ours = daemon_busy_;
|
||||||
return daemon::shouldConfirmStop(stops, ours, rpcBusyWhileAlive());
|
return daemon::shouldConfirmStop(true, ours, rpcBusyWhileAlive());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool App::refuseIfNodeBusy()
|
||||||
|
{
|
||||||
|
if (!nodeBusyForStop()) return false;
|
||||||
|
DEBUG_LOGF("[App] refused: the node is busy (%s) — stopping it now would make it start over\n",
|
||||||
|
daemon_busy_.rebuilding() ? "rebuilding witnesses" : "not answering RPC");
|
||||||
|
ui::Notifications::instance().warning(TR("appx_node_busy_try_later"), 10.0f);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::renderDaemonStopConfirm()
|
void App::renderDaemonStopConfirm()
|
||||||
@@ -7295,6 +7306,7 @@ void App::restartDaemon()
|
|||||||
daemon::DaemonController::LifecycleOperation::ManualRestart,
|
daemon::DaemonController::LifecycleOperation::ManualRestart,
|
||||||
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning(), daemon_restarting_.load());
|
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning(), daemon_restarting_.load());
|
||||||
if (!decision.allowed) return;
|
if (!decision.allowed) return;
|
||||||
|
if (refuseIfNodeBusy()) return;
|
||||||
daemon_restarting_ = true;
|
daemon_restarting_ = true;
|
||||||
|
|
||||||
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
||||||
@@ -7314,8 +7326,14 @@ void App::restartDaemon()
|
|||||||
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
|
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
|
||||||
AppDaemonLifecycleRuntime runtime(*this);
|
AppDaemonLifecycleRuntime runtime(*this);
|
||||||
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
||||||
daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
const auto result = daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
||||||
|
// Re-arm the connect loop either way: it reconnects to the new node, or to the old one still running.
|
||||||
daemon_restarting_ = false;
|
daemon_restarting_ = false;
|
||||||
|
if (result.stopFailed) {
|
||||||
|
DEBUG_LOGF("[App] Daemon restart cancelled: the node did not exit; not starting a second one\n");
|
||||||
|
pending_stop_failed_.fetch_or(kStopFailedNotify);
|
||||||
|
return;
|
||||||
|
}
|
||||||
DEBUG_LOGF("[App] Daemon restart complete — waiting for RPC...\n");
|
DEBUG_LOGF("[App] Daemon restart complete — waiting for RPC...\n");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,12 @@ public:
|
|||||||
// The node is alive and its RPC has only timed out / been refused as busy for >= 30s (a rebuild that
|
// The node is alive and its RPC has only timed out / been refused as busy for >= 30s (a rebuild that
|
||||||
// logged no marker, e.g. an external node whose output we can't read).
|
// logged no marker, e.g. an external node whose output we can't read).
|
||||||
bool rpcBusyWhileAlive() const;
|
bool rpcBusyWhileAlive() const;
|
||||||
|
// The node is rebuilding witnesses (our own node's log says so) or has been too busy to answer for a
|
||||||
|
// while: stopping it now either waits up to an hour or makes it start over. UI thread only.
|
||||||
|
bool nodeBusyForStop() const;
|
||||||
|
// For an operation that would stop the node (restart, rescan, switch, reinstall, bootstrap, ...): when
|
||||||
|
// nodeBusyForStop(), tell the user to try again later and return true; the caller then does nothing.
|
||||||
|
bool refuseIfNodeBusy();
|
||||||
// Record how an RPC to the node just went, for rpcBusyWhileAlive(): busy kinds (ReplyTimeout /
|
// Record how an RPC to the node just went, for rpcBusyWhileAlive(): busy kinds (ReplyTimeout /
|
||||||
// QueueFull) start the clock, an answer stops it, anything else leaves it alone.
|
// QueueFull) start the clock, an answer stops it, anything else leaves it alone.
|
||||||
void noteDaemonRpcResult(bool answered, rpc::RpcFailureKind kind);
|
void noteDaemonRpcResult(bool answered, rpc::RpcFailureKind kind);
|
||||||
@@ -497,12 +503,16 @@ public:
|
|||||||
Background, // restart/rescan/switch/...: up to 120s, and let go as soon as the app starts quitting
|
Background, // restart/rescan/switch/...: up to 120s, and let go as soon as the app starts quitting
|
||||||
Fallback, // the synchronous shutdown fallback on the UI thread: up to 20s
|
Fallback, // the synchronous shutdown fallback on the UI thread: up to 20s
|
||||||
};
|
};
|
||||||
daemon::StopOutcome stopEmbeddedDaemon(StopMode mode = StopMode::Background);
|
// Which node it may stop. Policy: our own, or an external one only when the "stop external daemon"
|
||||||
// Stop the node specifically for a wallet switch, which MUST free the RPC port to relaunch on
|
// setting says so. Connected: also the node we're connected to (adopted or external), with the
|
||||||
// -wallet=<name>. Unlike stopEmbeddedDaemon() (whose DisconnectOnly policy leaves an adopted/external
|
// credentials we connected with — a wallet switch, a reinstall and decrypt must free the port, the binary
|
||||||
// daemon running), this sends a graceful RPC "stop" — using our own connection creds, so only our
|
// and wallet.dat. Only a node that accepts those credentials obeys.
|
||||||
// daemon obeys it — even to an adopted daemon, then waits (bounded) for the port to actually release.
|
enum class StopScope { Policy, Connected };
|
||||||
// Returns true once the RPC port is free (or we're shutting down).
|
daemon::StopOutcome stopEmbeddedDaemon(StopMode mode = StopMode::Background, StopScope scope = StopScope::Policy);
|
||||||
|
// Stop the node for a wallet switch / adopt / restore / rebuild, which swap wallet.dat or relaunch on
|
||||||
|
// -wallet=<name>. Unlike stopEmbeddedDaemon()'s default policy (which leaves an adopted/external node
|
||||||
|
// running) it stops the node we're connected to too (StopScope::Connected). True only once the process
|
||||||
|
// has exited and the RPC port is free; false if it is still running, or the app started quitting.
|
||||||
bool stopDaemonForWalletSwitch();
|
bool stopDaemonForWalletSwitch();
|
||||||
bool isEmbeddedDaemonRunning() const;
|
bool isEmbeddedDaemonRunning() const;
|
||||||
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
|
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
|
||||||
@@ -1078,6 +1088,10 @@ private:
|
|||||||
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
||||||
std::atomic<int> pending_delete_result_{-1};
|
std::atomic<int> pending_delete_result_{-1};
|
||||||
std::atomic<bool> pending_reinstall_node_busy_{false}; // reinstall gave up: the old node never exited
|
std::atomic<bool> pending_reinstall_node_busy_{false}; // reinstall gave up: the old node never exited
|
||||||
|
// A worker stopped the node for an operation and it didn't exit: the operation was cancelled. Told (and,
|
||||||
|
// for a rescan/repair, its armed state undone) on the UI thread. Bits: kStopFailed*.
|
||||||
|
static constexpr int kStopFailedNotify = 1, kStopFailedUndoRescan = 2;
|
||||||
|
std::atomic<int> pending_stop_failed_{0};
|
||||||
|
|
||||||
// Encryption state check timeout
|
// Encryption state check timeout
|
||||||
float encryption_check_timer_ = 0.0f;
|
float encryption_check_timer_ = 0.0f;
|
||||||
|
|||||||
@@ -1391,6 +1391,7 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
|
|||||||
ui::Notifications::instance().warning("Finish or cancel the pending send before switching wallets.");
|
ui::Notifications::instance().warning("Finish or cancel the pending send before switching wallets.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (refuseIfNodeBusy()) return;
|
||||||
// W1-1: verify the target wallet file actually exists before switching. dragonxd auto-CREATES a
|
// W1-1: verify the target wallet file actually exists before switching. dragonxd auto-CREATES a
|
||||||
// fresh empty wallet for a missing -wallet=<name>, so without this a moved/deleted wallet file would
|
// fresh empty wallet for a missing -wallet=<name>, so without this a moved/deleted wallet file would
|
||||||
// silently "open" as a brand-new empty wallet with a zero balance — looking exactly like fund loss.
|
// silently "open" as a brand-new empty wallet with a zero balance — looking exactly like fund loss.
|
||||||
@@ -5071,6 +5072,7 @@ void App::locateSweepTxid()
|
|||||||
// restart with -rescan so the new keys pick up the swept funds. Runs on a background thread.
|
// restart with -rescan so the new keys pick up the swept funds. Runs on a background thread.
|
||||||
void App::beginAdoptSeedWallet()
|
void App::beginAdoptSeedWallet()
|
||||||
{
|
{
|
||||||
|
if (refuseIfNodeBusy()) return; // stays on its current step; the user retries once the node is free
|
||||||
seed_migration_step_ = SeedMigrationStep::Adopting;
|
seed_migration_step_ = SeedMigrationStep::Adopting;
|
||||||
seed_migration_status_ = "Restarting with your new wallet — this rescan can take a while…";
|
seed_migration_status_ = "Restarting with your new wallet — this rescan can take a while…";
|
||||||
{ std::lock_guard<std::mutex> lk(seed_migration_mutex_); seed_migration_adopt_done_ = false; }
|
{ std::lock_guard<std::mutex> lk(seed_migration_mutex_); seed_migration_adopt_done_ = false; }
|
||||||
@@ -5186,6 +5188,7 @@ void App::restoreOriginalWallet()
|
|||||||
ui::Notifications::instance().warning(TR("wallet_restore_busy"));
|
ui::Notifications::instance().warning(TR("wallet_restore_busy"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (refuseIfNodeBusy()) return;
|
||||||
// Keep the recovery dialog OPEN and drive it into the Working phase — it shows progress and the
|
// Keep the recovery dialog OPEN and drive it into the Working phase — it shows progress and the
|
||||||
// honest outcome in place (pumpWalletRestore flips it to Done/Failed). Presentation only; every
|
// honest outcome in place (pumpWalletRestore flips it to Done/Failed). Presentation only; every
|
||||||
// file-safety step below is unchanged.
|
// file-safety step below is unchanged.
|
||||||
@@ -5353,6 +5356,7 @@ void App::rebuildWalletDatabase()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (daemon_restarting_) { ui::Notifications::instance().warning(TR("wallet_restore_busy")); return; }
|
if (daemon_restarting_) { ui::Notifications::instance().warning(TR("wallet_restore_busy")); return; }
|
||||||
|
if (refuseIfNodeBusy()) return;
|
||||||
const std::string helper = findWalletRebuildHelper();
|
const std::string helper = findWalletRebuildHelper();
|
||||||
if (helper.empty()) { ui::Notifications::instance().error(TR("wallet_rebuild_no_helper"), 15.0f); return; }
|
if (helper.empty()) { ui::Notifications::instance().error(TR("wallet_rebuild_no_helper"), 15.0f); return; }
|
||||||
|
|
||||||
|
|||||||
+24
-13
@@ -214,7 +214,7 @@ public:
|
|||||||
: app_(app), token_(token) {}
|
: app_(app), token_(token) {}
|
||||||
|
|
||||||
bool isUsingEmbeddedDaemon() const override { return app_.isUsingEmbeddedDaemon(); }
|
bool isUsingEmbeddedDaemon() const override { return app_.isUsingEmbeddedDaemon(); }
|
||||||
void stopEmbeddedDaemon() override { app_.stopEmbeddedDaemon(); }
|
bool stopEmbeddedDaemon() override { return app_.stopEmbeddedDaemon() == daemon::StopOutcome::Exited; }
|
||||||
bool startEmbeddedDaemon() override { return app_.startEmbeddedDaemon(); }
|
bool startEmbeddedDaemon() override { return app_.startEmbeddedDaemon(); }
|
||||||
bool cancelled() const override { return token_.cancelled(); }
|
bool cancelled() const override { return token_.cancelled(); }
|
||||||
bool shuttingDown() const override { return app_.isShuttingDown(); }
|
bool shuttingDown() const override { return app_.isShuttingDown(); }
|
||||||
@@ -292,8 +292,13 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
|
|||||||
for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i)
|
for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i)
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
if (!token.cancelled() && !shutting_down_) {
|
if (!token.cancelled() && !shutting_down_) {
|
||||||
stopEmbeddedDaemon();
|
if (stopEmbeddedDaemon() == daemon::StopOutcome::Exited) {
|
||||||
if (!token.cancelled() && !shutting_down_) startEmbeddedDaemon();
|
if (!token.cancelled() && !shutting_down_) startEmbeddedDaemon();
|
||||||
|
} else if (!shutting_down_) {
|
||||||
|
// Still running: never start a second node over it. The connect loop goes back to it.
|
||||||
|
DEBUG_LOGF("[App] encryption restart: the node did not exit; not restarting it\n");
|
||||||
|
pending_stop_failed_.fetch_or(kStopFailedNotify);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
daemon_restarting_ = false; // re-arm reconnect (tryConnect runs from the update loop)
|
daemon_restarting_ = false; // re-arm reconnect (tryConnect runs from the update loop)
|
||||||
@@ -306,6 +311,7 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
|
|||||||
|
|
||||||
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||||
if (!rpc_ || !rpc_->isConnected()) return;
|
if (!rpc_ || !rpc_->isConnected()) return;
|
||||||
|
if (refuseIfNodeBusy()) return; // encrypting ends in a node restart
|
||||||
encrypt_in_progress_ = true;
|
encrypt_in_progress_ = true;
|
||||||
encrypt_status_ = TR("sec_encrypting_wallet");
|
encrypt_status_ = TR("sec_encrypting_wallet");
|
||||||
|
|
||||||
@@ -1561,7 +1567,9 @@ void App::renderDecryptWalletDialog() {
|
|||||||
|
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
ImGui::BeginDisabled(!valid || decryptState.inProgress);
|
ImGui::BeginDisabled(!valid || decryptState.inProgress);
|
||||||
if (ui::material::TactileButton(TR("settings_remove_encryption"), ImVec2(btnW, 40)) || (enterPressed && valid)) {
|
// Decrypting stops and restarts the node: not while it is busy (refuseIfNodeBusy() only on a click).
|
||||||
|
if ((ui::material::TactileButton(TR("settings_remove_encryption"), ImVec2(btnW, 40)) || (enterPressed && valid)) &&
|
||||||
|
!refuseIfNodeBusy()) {
|
||||||
std::string passphrase(decrypt_pass_buf_);
|
std::string passphrase(decrypt_pass_buf_);
|
||||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||||
wallet_security_workflow_.start(std::chrono::steady_clock::now());
|
wallet_security_workflow_.start(std::chrono::steady_clock::now());
|
||||||
@@ -1613,15 +1621,18 @@ void App::renderDecryptWalletDialog() {
|
|||||||
|
|
||||||
// Continue with step 3
|
// Continue with step 3
|
||||||
worker_->post([this, filePlan]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this, filePlan]() -> rpc::RPCWorker::MainCb {
|
||||||
WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
|
// Stop the node and wait for its PROCESS to exit before wallet.dat is
|
||||||
[this](rpc::RPCClient& client, const char* context) {
|
// renamed: dragonxd keeps the file open until it exits, which can take
|
||||||
return sendStopCommandSafely(client, context);
|
// minutes on a large wallet (a fixed 3s wait let the rename run under a
|
||||||
});
|
// live node). Ours or the one we're connected to; bounded, never killed.
|
||||||
services::WalletSecurityWorkflowExecutor::stopDaemon(decryptRpc);
|
const bool stopped = stopEmbeddedDaemon(StopMode::Background, StopScope::Connected) ==
|
||||||
|
daemon::StopOutcome::Exited;
|
||||||
// Wait for daemon to fully stop
|
if (!stopped) {
|
||||||
for (int i = 0; i < 30 && !shutting_down_; ++i)
|
App::scrubAndRemoveExport(filePlan.exportPath); // nothing will import it
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
return [this]() {
|
||||||
|
wallet_security_workflow_.fail(TR("appx_node_not_stopped"));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return [this, filePlan]() {
|
return [this, filePlan]() {
|
||||||
wallet_security_workflow_.advanceTo(DecryptStep::BackupWallet,
|
wallet_security_workflow_.advanceTo(DecryptStep::BackupWallet,
|
||||||
|
|||||||
+9
-5
@@ -77,6 +77,7 @@ void App::restartWizard()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (refuseIfNodeBusy()) return; // the wizard stops the node
|
||||||
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
|
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
|
||||||
|
|
||||||
// Reset crash counter for fresh wizard attempt
|
// Reset crash counter for fresh wizard attempt
|
||||||
@@ -874,7 +875,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
if (ui::material::TactileButton(TR("retry"), ImVec2(btnW2, btnH2))) {
|
if (ui::material::TactileButton(TR("retry"), ImVec2(btnW2, btnH2)) && !refuseIfNodeBusy()) {
|
||||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||||
stopDaemonForBootstrap();
|
stopDaemonForBootstrap();
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
@@ -974,9 +975,12 @@ void App::renderFirstRunWizard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
wizard_stop_status_ = TR("wiz_daemon_waiting_stop");
|
wizard_stop_status_ = TR("wiz_daemon_waiting_stop");
|
||||||
for (int i = 0; i < 60 && !token.cancelled(); i++) {
|
// Stopped means the PROCESS is gone, not just its RPC port: dragonxd unbinds RPC
|
||||||
|
// early in its shutdown and holds the datadir lock until it exits.
|
||||||
|
for (int i = 0; i < 120 && !token.cancelled(); i++) {
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
if (!daemon::EmbeddedDaemon::isRpcPortInUse() &&
|
||||||
|
!daemon::EmbeddedDaemon::isDaemonProcessRunning()) {
|
||||||
wizard_stop_status_ = TR("wiz_daemon_stopped_ok");
|
wizard_stop_status_ = TR("wiz_daemon_stopped_ok");
|
||||||
wizard_stopping_external_ = false;
|
wizard_stopping_external_ = false;
|
||||||
return;
|
return;
|
||||||
@@ -1058,7 +1062,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
if (ui::material::TactileButton(TR("download"), ImVec2(dlBtnW, btnH2))) {
|
if (ui::material::TactileButton(TR("download"), ImVec2(dlBtnW, btnH2)) && !refuseIfNodeBusy()) {
|
||||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||||
stopDaemonForBootstrap();
|
stopDaemonForBootstrap();
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
@@ -1077,7 +1081,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
if (ui::material::TactileButton(TR("bootstrap_mirror"), ImVec2(mirrorW, btnH2))) {
|
if (ui::material::TactileButton(TR("bootstrap_mirror"), ImVec2(mirrorW, btnH2)) && !refuseIfNodeBusy()) {
|
||||||
stopDaemonForBootstrap();
|
stopDaemonForBootstrap();
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||||
|
|||||||
@@ -63,17 +63,21 @@ public:
|
|||||||
class LifecycleRuntime {
|
class LifecycleRuntime {
|
||||||
public:
|
public:
|
||||||
virtual ~LifecycleRuntime() = default;
|
virtual ~LifecycleRuntime() = default;
|
||||||
virtual void stopDaemonWithPolicy() = 0;
|
// Stop the node and wait (bounded, never killing it) for it to exit. False: it is still running
|
||||||
|
// (or we stopped waiting), and nothing that needs it gone may run.
|
||||||
|
virtual bool stopDaemonWithPolicy() = 0;
|
||||||
virtual bool startDaemon() = 0;
|
virtual bool startDaemon() = 0;
|
||||||
virtual int deleteBlockchainData() = 0;
|
virtual int deleteBlockchainData() = 0;
|
||||||
virtual void resetOutputOffset() = 0;
|
virtual void resetOutputOffset() = 0;
|
||||||
virtual void requestRpcStopAndDisconnect(const char* context, const char* reason) = 0;
|
// Disconnect and stop the node off this thread; whoever needs it gone waits for the process to exit.
|
||||||
|
virtual void beginBackgroundStop(const char* reason) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LifecycleExecutionResult {
|
struct LifecycleExecutionResult {
|
||||||
bool completed = false;
|
bool completed = false;
|
||||||
bool cancelled = false;
|
bool cancelled = false;
|
||||||
bool stopped = false;
|
bool stopped = false;
|
||||||
|
bool stopFailed = false; // the node didn't exit: the operation was cancelled before changing anything
|
||||||
bool started = false;
|
bool started = false;
|
||||||
int deletedItems = 0;
|
int deletedItems = 0;
|
||||||
};
|
};
|
||||||
@@ -194,22 +198,24 @@ public:
|
|||||||
|
|
||||||
switch (decision.operation) {
|
switch (decision.operation) {
|
||||||
case LifecycleOperation::BootstrapStop:
|
case LifecycleOperation::BootstrapStop:
|
||||||
|
// The download takes minutes; the bootstrap worker waits for the process to exit before it
|
||||||
|
// touches chain data.
|
||||||
if (decision.wasRunning) {
|
if (decision.wasRunning) {
|
||||||
runtime.requestRpcStopAndDisconnect("Bootstrap daemon stop", "Bootstrap");
|
runtime.beginBackgroundStop("Bootstrap");
|
||||||
result.stopped = true;
|
result.stopped = true;
|
||||||
}
|
}
|
||||||
result.completed = true;
|
result.completed = true;
|
||||||
return result;
|
return result;
|
||||||
case LifecycleOperation::ManualRestart:
|
case LifecycleOperation::ManualRestart:
|
||||||
if (decision.wasRunning) {
|
|
||||||
runtime.stopDaemonWithPolicy();
|
|
||||||
result.stopped = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case LifecycleOperation::Rescan:
|
case LifecycleOperation::Rescan:
|
||||||
case LifecycleOperation::RepairWallet:
|
case LifecycleOperation::RepairWallet:
|
||||||
case LifecycleOperation::DeleteBlockchainData:
|
case LifecycleOperation::DeleteBlockchainData:
|
||||||
runtime.stopDaemonWithPolicy();
|
if (decision.operation == LifecycleOperation::ManualRestart && !decision.wasRunning) break;
|
||||||
|
// Never start a second node, or delete chain data, under one that hasn't exited.
|
||||||
|
if (!runtime.stopDaemonWithPolicy()) {
|
||||||
|
result.stopFailed = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
result.stopped = true;
|
result.stopped = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -255,6 +261,9 @@ public:
|
|||||||
bool abandonOnShutdown = false; // stop waiting once the app starts shutting down (background callers)
|
bool abandonOnShutdown = false; // stop waiting once the app starts shutting down (background callers)
|
||||||
double idleOfferSec = 600.0; // delivered + no CPU/I-O progress for this long: offer Kill
|
double idleOfferSec = 600.0; // delivered + no CPU/I-O progress for this long: offer Kill
|
||||||
};
|
};
|
||||||
|
// A bounded stop that has no way to reach the node (a node we don't own refused our credentials, or our
|
||||||
|
// own did with every source and there is no signal) gives up after this long rather than its full budget.
|
||||||
|
static constexpr double kUndeliverableGraceSec = 5.0;
|
||||||
|
|
||||||
// What the stop loop reports for display.
|
// What the stop loop reports for display.
|
||||||
struct StopStatus {
|
struct StopStatus {
|
||||||
@@ -342,6 +351,7 @@ public:
|
|||||||
StopUserChoice choice = StopUserChoice::None;
|
StopUserChoice choice = StopUserChoice::None;
|
||||||
bool killOffered = false; // the Kill offer is on screen right now
|
bool killOffered = false; // the Kill offer is on screen right now
|
||||||
bool shuttingDown = false; // the app began shutting down / the task was cancelled
|
bool shuttingDown = false; // the app began shutting down / the task was cancelled
|
||||||
|
bool undeliverable = false; // no stop delivered, none will be sent, and no signal to send
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline StopWaitDecision evaluateStopWait(const StopPolicy& p, const StopWaitSample& s)
|
static inline StopWaitDecision evaluateStopWait(const StopPolicy& p, const StopWaitSample& s)
|
||||||
@@ -352,6 +362,8 @@ public:
|
|||||||
return StopWaitDecision::ForceKill;
|
return StopWaitDecision::ForceKill;
|
||||||
if (p.abandonOnShutdown && s.shuttingDown) return StopWaitDecision::GiveUp;
|
if (p.abandonOnShutdown && s.shuttingDown) return StopWaitDecision::GiveUp;
|
||||||
if (p.maxWaitSec > 0 && s.elapsedSec >= p.maxWaitSec) return StopWaitDecision::GiveUp;
|
if (p.maxWaitSec > 0 && s.elapsedSec >= p.maxWaitSec) return StopWaitDecision::GiveUp;
|
||||||
|
if (p.maxWaitSec > 0 && s.undeliverable && s.elapsedSec >= kUndeliverableGraceSec)
|
||||||
|
return StopWaitDecision::GiveUp;
|
||||||
if (p.userChoices && p.owned && s.delivered && !s.rebuildActive && s.idleSec >= p.idleOfferSec)
|
if (p.userChoices && p.owned && s.delivered && !s.rebuildActive && s.idleSec >= p.idleOfferSec)
|
||||||
return StopWaitDecision::OfferForceKill;
|
return StopWaitDecision::OfferForceKill;
|
||||||
return StopWaitDecision::Wait;
|
return StopWaitDecision::Wait;
|
||||||
@@ -406,6 +418,8 @@ public:
|
|||||||
s.choice = rt.userChoice();
|
s.choice = rt.userChoice();
|
||||||
s.killOffered = offered;
|
s.killOffered = offered;
|
||||||
s.shuttingDown = task.cancelled() || task.shuttingDown();
|
s.shuttingDown = task.cancelled() || task.shuttingDown();
|
||||||
|
s.undeliverable = delivery.finished() && !delivery.delivered() && !signalled &&
|
||||||
|
!(policy.gracefulSignal && policy.owned);
|
||||||
|
|
||||||
switch (evaluateStopWait(policy, s)) {
|
switch (evaluateStopWait(policy, s)) {
|
||||||
case StopWaitDecision::Done:
|
case StopWaitDecision::Done:
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
#include <netinet/in.h>
|
#include <netinet/in.h>
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
|
#include <libproc.h>
|
||||||
#include <sys/sysctl.h>
|
#include <sys/sysctl.h>
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
@@ -1664,8 +1665,20 @@ bool EmbeddedDaemon::isDaemonProcessRunning()
|
|||||||
if (isDaemonProcessIdentity(comm, exe)) return true;
|
if (isDaemonProcessIdentity(comm, exe)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
// Match executables named dragonxd. Not the RPC port: dragonxd frees it early in its shutdown, long
|
||||||
|
// before the process exits and lets go of wallet.dat and the datadir lock.
|
||||||
|
const int count = proc_listallpids(nullptr, 0);
|
||||||
|
if (count <= 0) return isPortInUse(std::atoi(DRAGONX_DEFAULT_RPC_PORT)); // can't list: best effort
|
||||||
|
std::vector<pid_t> pids(static_cast<size_t>(count) + 64); // room for processes started meanwhile
|
||||||
|
const int n = proc_listallpids(pids.data(), static_cast<int>(pids.size() * sizeof(pid_t)));
|
||||||
|
char path[PROC_PIDPATHINFO_MAXSIZE];
|
||||||
|
for (int i = 0; i < n && i < static_cast<int>(pids.size()); ++i) {
|
||||||
|
if (pids[i] <= 0 || proc_pidpath(pids[i], path, sizeof(path)) <= 0) continue;
|
||||||
|
if (isDaemonProcessIdentity("", fs::path(path).filename().string())) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
#else
|
#else
|
||||||
// macOS has no /proc; fall back to the RPC-port probe (best-effort).
|
|
||||||
return isPortInUse(std::atoi(DRAGONX_DEFAULT_RPC_PORT));
|
return isPortInUse(std::atoi(DRAGONX_DEFAULT_RPC_PORT));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::restartD
|
|||||||
if (!waitForMs(preRestartDelayMs)) return {false, "", false};
|
if (!waitForMs(preRestartDelayMs)) return {false, "", false};
|
||||||
|
|
||||||
if (daemon.isUsingEmbeddedDaemon()) {
|
if (daemon.isUsingEmbeddedDaemon()) {
|
||||||
daemon.stopEmbeddedDaemon();
|
const bool stopped = daemon.stopEmbeddedDaemon();
|
||||||
if (daemon.cancelled() || daemon.shuttingDown()) return {false, "", false};
|
if (daemon.cancelled() || daemon.shuttingDown()) return {false, "", false};
|
||||||
|
// Never start a second node over one that hasn't exited (it holds wallet.dat and the datadir lock).
|
||||||
|
if (!stopped) return {false, "The node did not stop in time, so it was not restarted.", false};
|
||||||
if (!waitForMs(embeddedRestartSettleMs)) return {false, "", false};
|
if (!waitForMs(embeddedRestartSettleMs)) return {false, "", false};
|
||||||
daemon.startEmbeddedDaemon();
|
daemon.startEmbeddedDaemon();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public:
|
|||||||
public:
|
public:
|
||||||
virtual ~DaemonGateway() = default;
|
virtual ~DaemonGateway() = default;
|
||||||
virtual bool isUsingEmbeddedDaemon() const = 0;
|
virtual bool isUsingEmbeddedDaemon() const = 0;
|
||||||
virtual void stopEmbeddedDaemon() = 0;
|
virtual bool stopEmbeddedDaemon() = 0; // true: the node exited (false: still running; never killed)
|
||||||
virtual bool startEmbeddedDaemon() = 0;
|
virtual bool startEmbeddedDaemon() = 0;
|
||||||
virtual bool cancelled() const = 0;
|
virtual bool cancelled() const = 0;
|
||||||
virtual bool shuttingDown() const = 0;
|
virtual bool shuttingDown() const = 0;
|
||||||
|
|||||||
@@ -162,11 +162,17 @@ void Bootstrap::start(const std::string& dataDir, const std::string& url) {
|
|||||||
worker_running_ = false;
|
worker_running_ = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Step 3: Ensure daemon is fully stopped before touching chain data
|
// Step 3: Ensure daemon is fully stopped before touching chain data. Gone means the PROCESS has
|
||||||
|
// exited, not just its RPC port: dragonxd unbinds RPC early in its shutdown and keeps its files
|
||||||
|
// (and the datadir lock) until it exits — deleting chain data under it corrupts it, or fails on
|
||||||
|
// Windows' locked files.
|
||||||
{
|
{
|
||||||
setProgress(State::Extracting, "Waiting for daemon to stop...");
|
setProgress(State::Extracting, "Waiting for daemon to stop...");
|
||||||
|
auto daemonUp = [] {
|
||||||
|
return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning();
|
||||||
|
};
|
||||||
int waited = 0;
|
int waited = 0;
|
||||||
while (daemon::EmbeddedDaemon::isRpcPortInUse() && waited < 60 && !cancel_requested_) {
|
while (daemonUp() && waited < 120 && !cancel_requested_) {
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
waited++;
|
waited++;
|
||||||
if (waited % 5 == 0)
|
if (waited % 5 == 0)
|
||||||
@@ -177,7 +183,7 @@ void Bootstrap::start(const std::string& dataDir, const std::string& url) {
|
|||||||
worker_running_ = false;
|
worker_running_ = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
if (daemonUp()) {
|
||||||
setProgress(State::Failed, "Daemon is still running — stop it before using bootstrap");
|
setProgress(State::Failed, "Daemon is still running — stop it before using bootstrap");
|
||||||
worker_running_ = false;
|
worker_running_ = false;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -616,6 +616,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["stopconfirm_busy_body"] = "It has been too busy to answer the wallet for %s, probably because of a long operation such as a witness rebuild. Stopping it now can make its next start redo that work.\n\nRecommended: close the wallet and leave the node running. It finishes on its own.";
|
strings_["stopconfirm_busy_body"] = "It has been too busy to answer the wallet for %s, probably because of a long operation such as a witness rebuild. Stopping it now can make its next start redo that work.\n\nRecommended: close the wallet and leave the node running. It finishes on its own.";
|
||||||
strings_["stopconfirm_stop_wait"] = "Stop node & wait";
|
strings_["stopconfirm_stop_wait"] = "Stop node & wait";
|
||||||
strings_["appx_daemon_reinstall_node_busy"] = "The node is still running, so the daemon was not reinstalled. Try again once it has stopped.";
|
strings_["appx_daemon_reinstall_node_busy"] = "The node is still running, so the daemon was not reinstalled. Try again once it has stopped.";
|
||||||
|
strings_["appx_node_busy_try_later"] = "The node is busy finishing a long task (such as rebuilding note witnesses). Try again when it is done; stopping it now could make it start that work over.";
|
||||||
|
strings_["appx_node_not_stopped"] = "The node didn't stop in time, so nothing was changed. Try again in a few minutes.";
|
||||||
strings_["reduce_motion"] = "Reduce Motion";
|
strings_["reduce_motion"] = "Reduce Motion";
|
||||||
strings_["tt_reduce_motion"] = "Disable animated transitions and balance lerp for accessibility";
|
strings_["tt_reduce_motion"] = "Disable animated transitions and balance lerp for accessibility";
|
||||||
strings_["ago"] = "ago";
|
strings_["ago"] = "ago";
|
||||||
|
|||||||
+82
-8
@@ -465,10 +465,14 @@ public:
|
|||||||
std::vector<std::string> calls;
|
std::vector<std::string> calls;
|
||||||
int deletedItems = 7;
|
int deletedItems = 7;
|
||||||
bool startResult = true;
|
bool startResult = true;
|
||||||
std::string rpcStopContext;
|
bool stopResult = true; // false: the node didn't exit
|
||||||
std::string disconnectReason;
|
std::string disconnectReason;
|
||||||
|
|
||||||
void stopDaemonWithPolicy() override { calls.push_back("stop"); }
|
bool stopDaemonWithPolicy() override
|
||||||
|
{
|
||||||
|
calls.push_back("stop");
|
||||||
|
return stopResult;
|
||||||
|
}
|
||||||
bool startDaemon() override
|
bool startDaemon() override
|
||||||
{
|
{
|
||||||
calls.push_back("start");
|
calls.push_back("start");
|
||||||
@@ -480,10 +484,9 @@ public:
|
|||||||
return deletedItems;
|
return deletedItems;
|
||||||
}
|
}
|
||||||
void resetOutputOffset() override { calls.push_back("reset-output"); }
|
void resetOutputOffset() override { calls.push_back("reset-output"); }
|
||||||
void requestRpcStopAndDisconnect(const char* context, const char* reason) override
|
void beginBackgroundStop(const char* reason) override
|
||||||
{
|
{
|
||||||
calls.push_back("rpc-stop-disconnect");
|
calls.push_back("background-stop");
|
||||||
rpcStopContext = context ? context : "";
|
|
||||||
disconnectReason = reason ? reason : "";
|
disconnectReason = reason ? reason : "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -599,9 +602,10 @@ public:
|
|||||||
int sleptMs = 0;
|
int sleptMs = 0;
|
||||||
int stopCalls = 0;
|
int stopCalls = 0;
|
||||||
int startCalls = 0;
|
int startCalls = 0;
|
||||||
|
bool stopResult = true;
|
||||||
|
|
||||||
bool isUsingEmbeddedDaemon() const override { return usingEmbedded; }
|
bool isUsingEmbeddedDaemon() const override { return usingEmbedded; }
|
||||||
void stopEmbeddedDaemon() override { ++stopCalls; }
|
bool stopEmbeddedDaemon() override { ++stopCalls; return stopResult; }
|
||||||
bool startEmbeddedDaemon() override { ++startCalls; return true; }
|
bool startEmbeddedDaemon() override { ++startCalls; return true; }
|
||||||
bool cancelled() const override { return isCancelled; }
|
bool cancelled() const override { return isCancelled; }
|
||||||
bool shuttingDown() const override { return isShuttingDown; }
|
bool shuttingDown() const override { return isShuttingDown; }
|
||||||
@@ -2841,6 +2845,15 @@ void testWalletSecurityWorkflowExecutor()
|
|||||||
EXPECT_FALSE(cancelled.ok);
|
EXPECT_FALSE(cancelled.ok);
|
||||||
EXPECT_TRUE(cancelled.error.empty());
|
EXPECT_TRUE(cancelled.error.empty());
|
||||||
|
|
||||||
|
MockWorkflowDaemon stuckDaemon; // S11: never start a second node over one that hasn't exited
|
||||||
|
stuckDaemon.stopResult = false;
|
||||||
|
rpc.probeCalls = 0;
|
||||||
|
auto stuckRestart = Executor::restartDaemonAndWait(stuckDaemon, rpc, 0, 0, 5);
|
||||||
|
EXPECT_FALSE(stuckRestart.ok);
|
||||||
|
EXPECT_FALSE(stuckRestart.error.empty());
|
||||||
|
EXPECT_EQ(stuckDaemon.startCalls, 0);
|
||||||
|
EXPECT_EQ(rpc.probeCalls, 0);
|
||||||
|
|
||||||
MockWorkflowDaemon failedDaemon;
|
MockWorkflowDaemon failedDaemon;
|
||||||
rpc.probeCalls = 0;
|
rpc.probeCalls = 0;
|
||||||
rpc.probeSuccessOnCall = 99;
|
rpc.probeSuccessOnCall = 99;
|
||||||
@@ -3868,6 +3881,13 @@ void testEvaluateStopWait()
|
|||||||
unowned.owned = false;
|
unowned.owned = false;
|
||||||
s = sample(); s.delivered = true; s.idleSec = 6000; s.killOffered = true; s.choice = DC::StopUserChoice::Kill;
|
s = sample(); s.delivered = true; s.idleSec = 6000; s.killOffered = true; s.choice = DC::StopUserChoice::Kill;
|
||||||
EXPECT_TRUE(DC::evaluateStopWait(unowned, s) == D::Wait); // never kill what we didn't launch
|
EXPECT_TRUE(DC::evaluateStopWait(unowned, s) == D::Wait); // never kill what we didn't launch
|
||||||
|
// S11: no way to reach the node (refused our credentials, no signal): a bounded stop gives up after
|
||||||
|
// a short grace instead of its whole budget; the shutdown screen keeps waiting (the user decides).
|
||||||
|
s = sample(); s.undeliverable = true; s.elapsedSec = DC::kUndeliverableGraceSec - 0.1;
|
||||||
|
EXPECT_TRUE(DC::evaluateStopWait(background, s) == D::Wait);
|
||||||
|
s.elapsedSec = DC::kUndeliverableGraceSec;
|
||||||
|
EXPECT_TRUE(DC::evaluateStopWait(background, s) == D::GiveUp);
|
||||||
|
EXPECT_TRUE(DC::evaluateStopWait(shutdown, s) == D::Wait);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A scripted node for executeDaemonStop, on a virtual clock (seconds) that sleeps and sends advance.
|
// A scripted node for executeDaemonStop, on a virtual clock (seconds) that sleeps and sends advance.
|
||||||
@@ -3977,6 +3997,30 @@ void testDaemonStopExecution()
|
|||||||
EXPECT_TRUE(DC::executeDaemonStop(shutdownStopPolicy(false), rt, task) == O::Exited);
|
EXPECT_TRUE(DC::executeDaemonStop(shutdownStopPolicy(false), rt, task) == O::Exited);
|
||||||
EXPECT_TRUE(rt.calls.empty());
|
EXPECT_TRUE(rt.calls.empty());
|
||||||
}
|
}
|
||||||
|
{ // S11: a wallet switch stopping a node we don't own that refuses our credentials (a foreign dragonxd):
|
||||||
|
// one attempt, then give up after the short grace, not the 120s budget. Never kill it.
|
||||||
|
MockStopRuntime rt; MockStopTask task(rt);
|
||||||
|
rt.sends = {R::Auth};
|
||||||
|
DC::StopPolicy p;
|
||||||
|
p.owned = false;
|
||||||
|
p.maxWaitSec = 120;
|
||||||
|
p.abandonOnShutdown = true;
|
||||||
|
EXPECT_TRUE(DC::executeDaemonStop(p, rt, task) == O::StillRunning);
|
||||||
|
EXPECT_EQ(rt.count("stop"), 1);
|
||||||
|
EXPECT_EQ(rt.count("kill"), 0);
|
||||||
|
EXPECT_TRUE(rt.clock >= DC::kUndeliverableGraceSec && rt.clock < DC::kUndeliverableGraceSec + 1.0);
|
||||||
|
}
|
||||||
|
{ // ...whereas our own node stays waited on (out of credentials, but SIGTERM still reaches it).
|
||||||
|
MockStopRuntime rt; MockStopTask task(rt);
|
||||||
|
rt.sends = {R::Auth};
|
||||||
|
rt.hasSignal = true;
|
||||||
|
rt.exitAfterDelivery = 20.0;
|
||||||
|
DC::StopPolicy p;
|
||||||
|
p.gracefulSignal = true;
|
||||||
|
p.maxWaitSec = 120;
|
||||||
|
EXPECT_TRUE(DC::executeDaemonStop(p, rt, task) == O::Exited);
|
||||||
|
EXPECT_EQ(rt.count("sigterm"), 1);
|
||||||
|
}
|
||||||
{ // The 2026-09-30 12:21:44 -> 12:22:04 regression: the queue is full for a while. Retry until it
|
{ // The 2026-09-30 12:21:44 -> 12:22:04 regression: the queue is full for a while. Retry until it
|
||||||
// lands (Windows-like, no signal channel), then wait for the exit. Never kill.
|
// lands (Windows-like, no signal channel), then wait for the exit. Never kill.
|
||||||
MockStopRuntime rt; MockStopTask task(rt);
|
MockStopRuntime rt; MockStopTask task(rt);
|
||||||
@@ -4379,6 +4423,7 @@ void testDaemonStopStringsTranslated()
|
|||||||
"node_busy_rpc_desc", "node_starting_title", "node_starting_busy_desc", "node_starting_launch_desc",
|
"node_busy_rpc_desc", "node_starting_title", "node_starting_busy_desc", "node_starting_launch_desc",
|
||||||
"sb_node_busy", "stopconfirm_rebuild_title", "stopconfirm_rebuild_body", "stopconfirm_busy_title",
|
"sb_node_busy", "stopconfirm_rebuild_title", "stopconfirm_rebuild_body", "stopconfirm_busy_title",
|
||||||
"stopconfirm_busy_body", "stopconfirm_stop_wait", "appx_daemon_reinstall_node_busy", "node_busy_running",
|
"stopconfirm_busy_body", "stopconfirm_stop_wait", "appx_daemon_reinstall_node_busy", "node_busy_running",
|
||||||
|
"appx_node_busy_try_later", "appx_node_not_stopped",
|
||||||
};
|
};
|
||||||
// printf conversions in order, "%%" excluded: what must survive translation.
|
// printf conversions in order, "%%" excluded: what must survive translation.
|
||||||
auto conversions = [](const std::string& s) {
|
auto conversions = [](const std::string& s) {
|
||||||
@@ -4483,10 +4528,39 @@ void testDaemonLifecycleExecution()
|
|||||||
auto bootstrapResult = DaemonController::executeLifecycleOperation(bootstrap, bootstrapRuntime, bootstrapTask);
|
auto bootstrapResult = DaemonController::executeLifecycleOperation(bootstrap, bootstrapRuntime, bootstrapTask);
|
||||||
EXPECT_TRUE(bootstrapResult.completed);
|
EXPECT_TRUE(bootstrapResult.completed);
|
||||||
EXPECT_TRUE(bootstrapResult.stopped);
|
EXPECT_TRUE(bootstrapResult.stopped);
|
||||||
EXPECT_TRUE(bootstrapRuntime.calls == std::vector<std::string>({"rpc-stop-disconnect"}));
|
EXPECT_TRUE(bootstrapRuntime.calls == std::vector<std::string>({"background-stop"}));
|
||||||
EXPECT_EQ(bootstrapRuntime.rpcStopContext, std::string("Bootstrap daemon stop"));
|
|
||||||
EXPECT_EQ(bootstrapRuntime.disconnectReason, std::string("Bootstrap"));
|
EXPECT_EQ(bootstrapRuntime.disconnectReason, std::string("Bootstrap"));
|
||||||
|
|
||||||
|
// S11: a node that doesn't exit cancels the operation before it changes anything: no second node is
|
||||||
|
// started over it, and chain data is not deleted under it.
|
||||||
|
for (auto op : {DaemonController::LifecycleOperation::ManualRestart, DaemonController::LifecycleOperation::Rescan,
|
||||||
|
DaemonController::LifecycleOperation::RepairWallet,
|
||||||
|
DaemonController::LifecycleOperation::DeleteBlockchainData}) {
|
||||||
|
auto decision = DaemonController::evaluateLifecycleOperation(op, true, true, true, false);
|
||||||
|
MockDaemonLifecycleRuntime stuckRuntime;
|
||||||
|
stuckRuntime.stopResult = false;
|
||||||
|
MockDaemonLifecycleTask stuckTask;
|
||||||
|
auto stuck = DaemonController::executeLifecycleOperation(decision, stuckRuntime, stuckTask);
|
||||||
|
EXPECT_TRUE(stuck.stopFailed);
|
||||||
|
EXPECT_FALSE(stuck.stopped);
|
||||||
|
EXPECT_FALSE(stuck.started);
|
||||||
|
EXPECT_FALSE(stuck.completed);
|
||||||
|
EXPECT_EQ(stuck.deletedItems, 0);
|
||||||
|
EXPECT_TRUE(stuckRuntime.calls == std::vector<std::string>({"stop"}));
|
||||||
|
EXPECT_EQ(stuckTask.sleeps, 0);
|
||||||
|
}
|
||||||
|
{ // A restart with no node of ours running just starts one (nothing to stop).
|
||||||
|
auto idle = DaemonController::evaluateLifecycleOperation(
|
||||||
|
DaemonController::LifecycleOperation::ManualRestart, true, true, false, false);
|
||||||
|
MockDaemonLifecycleRuntime idleRuntime;
|
||||||
|
idleRuntime.stopResult = false;
|
||||||
|
MockDaemonLifecycleTask idleTask;
|
||||||
|
auto idleResult = DaemonController::executeLifecycleOperation(idle, idleRuntime, idleTask);
|
||||||
|
EXPECT_FALSE(idleResult.stopFailed);
|
||||||
|
EXPECT_TRUE(idleResult.started);
|
||||||
|
EXPECT_TRUE(idleRuntime.calls == std::vector<std::string>({"start"}));
|
||||||
|
}
|
||||||
|
|
||||||
MockDaemonLifecycleRuntime cancelRuntime;
|
MockDaemonLifecycleRuntime cancelRuntime;
|
||||||
MockDaemonLifecycleTask cancelTask;
|
MockDaemonLifecycleTask cancelTask;
|
||||||
cancelTask.cancelAfterSleeps = 1;
|
cancelTask.cancelAfterSleeps = 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user