#pragma once #include "embedded_daemon.h" #include "stop_delivery.h" #include #include #include #include #include #include namespace dragonx { namespace config { class Settings; } namespace daemon { class DaemonController { public: using State = EmbeddedDaemon::State; using StateCallback = EmbeddedDaemon::StateCallback; enum class ShutdownAction { DisconnectOnly, StopDaemon }; struct ShutdownDecision { ShutdownAction action = ShutdownAction::DisconnectOnly; const char* logReason = "no embedded daemon"; const char* status = "Disconnecting..."; }; enum class LifecycleOperation { ManualRestart, Rescan, RepairWallet, // restart with -zapwallettxes=2 (wipe & rebuild wallet tx records) DeleteBlockchainData, BootstrapStop }; struct LifecycleDecision { LifecycleOperation operation = LifecycleOperation::ManualRestart; bool allowed = false; bool wasRunning = false; const char* taskName = ""; const char* status = ""; const char* warning = ""; bool resetCrashCount = false; bool setRescanOnNextStart = false; bool disconnectRpc = false; int restartDelayMs = 0; bool setZapOnNextStart = false; }; class LifecycleTaskContext { public: virtual ~LifecycleTaskContext() = default; virtual bool cancelled() const = 0; virtual bool shuttingDown() const = 0; virtual void sleepForMs(int milliseconds) = 0; }; class LifecycleRuntime { public: virtual ~LifecycleRuntime() = default; virtual void stopDaemonWithPolicy() = 0; virtual bool startDaemon() = 0; virtual int deleteBlockchainData() = 0; virtual void resetOutputOffset() = 0; virtual void requestRpcStopAndDisconnect(const char* context, const char* reason) = 0; }; struct LifecycleExecutionResult { bool completed = false; bool cancelled = false; bool stopped = false; bool started = false; int deletedItems = 0; }; DaemonController(); ~DaemonController(); DaemonController(const DaemonController&) = delete; DaemonController& operator=(const DaemonController&) = delete; EmbeddedDaemon* daemon() { return daemon_.get(); } const EmbeddedDaemon* daemon() const { return daemon_.get(); } void setStateCallback(StateCallback callback); void syncSettings(const config::Settings* settings); bool start(const config::Settings* settings); void stop(int waitMs); void markStopRequested(); bool isRunning() const; bool externalDaemonDetected() const; void clearExternalDaemonDetected(); State state() const; std::string lastError() const; // by value: EmbeddedDaemon::getLastError() returns a locked copy (M-04) int crashCount() const; int lastBlockHeight() const; double memoryUsageMB() const; std::vector recentLines(std::size_t count) const; std::string outputSince(std::size_t& offset) const; void resetCrashCount(); void setRescanOnNextStart(bool enabled); bool rescanOnNextStart() const; void setZapOnNextStart(bool enabled); bool zapOnNextStart() const; void setSalvageOnNextStart(bool enabled); void setReindexOnNextStart(bool enabled); // -reindex: rebuild the block DB from raw blocks on next start static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon, bool externalDaemonDetected, bool keepDaemonRunning, bool stopExternalDaemon) { if (!hasDaemon) { return {}; } if (keepDaemonRunning) { return {ShutdownAction::DisconnectOnly, "keep_daemon_running enabled", "Disconnecting (daemon stays running)..."}; } if (externalDaemonDetected && !stopExternalDaemon) { return {ShutdownAction::DisconnectOnly, "external daemon (not ours to stop)", "Disconnecting (daemon stays running)..."}; } return {ShutdownAction::StopDaemon, "stopping managed daemon", "Sending stop command to daemon..."}; } static LifecycleDecision evaluateLifecycleOperation(LifecycleOperation operation, bool usingEmbeddedDaemon, bool hasDaemon, bool daemonRunning, bool restartInProgress = false) { switch (operation) { case LifecycleOperation::ManualRestart: if (!usingEmbeddedDaemon || restartInProgress) return {}; return {operation, true, daemonRunning, "daemon-restart", "Restarting daemon...", "", true, false, true, 500}; case LifecycleOperation::Rescan: if (!usingEmbeddedDaemon || !hasDaemon) { return {operation, false, daemonRunning, "", "", "Rescan requires embedded daemon. Restart your daemon with -rescan manually."}; } return {operation, true, daemonRunning, "rescan-blockchain", "Starting rescan...", "", false, true, false, 3000}; case LifecycleOperation::RepairWallet: if (!usingEmbeddedDaemon || !hasDaemon) { return {operation, false, daemonRunning, "", "", "Wallet repair requires embedded daemon. Restart your daemon with -zapwallettxes=2 manually."}; } return {operation, true, daemonRunning, "repair-wallet", "Repairing wallet...", "", false, false, false, 3000, true}; case LifecycleOperation::DeleteBlockchainData: if (!usingEmbeddedDaemon || !hasDaemon) { return {operation, false, daemonRunning, "", "", "Delete blockchain requires embedded daemon. Stop your daemon manually and delete the data directory."}; } return {operation, true, daemonRunning, "delete-blockchain-data", "Deleting blockchain data...", "", false, false, false, 3000}; case LifecycleOperation::BootstrapStop: return {operation, true, daemonRunning, "bootstrap-stop-daemon", "Stopping daemon for bootstrap...", "", false, false, true, 0}; } return {}; } void prepareLifecycleOperation(const LifecycleDecision& decision, const config::Settings* settings = nullptr); static inline LifecycleExecutionResult executeLifecycleOperation(const LifecycleDecision& decision, LifecycleRuntime& runtime, LifecycleTaskContext& task) { LifecycleExecutionResult result; if (!decision.allowed) return result; auto waitForDelay = [&]() { int waitTicks = std::max(0, decision.restartDelayMs / 100); for (int i = 0; i < waitTicks && !task.cancelled() && !task.shuttingDown(); ++i) { task.sleepForMs(100); } }; auto cancelled = [&]() { result.cancelled = task.cancelled() || task.shuttingDown(); return result.cancelled; }; switch (decision.operation) { case LifecycleOperation::BootstrapStop: if (decision.wasRunning) { runtime.requestRpcStopAndDisconnect("Bootstrap daemon stop", "Bootstrap"); result.stopped = true; } result.completed = true; return result; case LifecycleOperation::ManualRestart: if (decision.wasRunning) { runtime.stopDaemonWithPolicy(); result.stopped = true; } break; case LifecycleOperation::Rescan: case LifecycleOperation::RepairWallet: case LifecycleOperation::DeleteBlockchainData: runtime.stopDaemonWithPolicy(); result.stopped = true; break; } if (cancelled()) return result; waitForDelay(); if (cancelled()) return result; if (decision.operation == LifecycleOperation::DeleteBlockchainData) { result.deletedItems = runtime.deleteBlockchainData(); if (cancelled()) return result; } if (decision.operation == LifecycleOperation::Rescan || decision.operation == LifecycleOperation::RepairWallet || decision.operation == LifecycleOperation::DeleteBlockchainData) { runtime.resetOutputOffset(); } result.started = runtime.startDaemon(); result.completed = !cancelled(); return result; } ShutdownDecision shutdownDecision(bool keepDaemonRunning, bool stopExternalDaemon) const; // ------------------------------------------------------------------------------------------------ // Stopping the node: deliver "stop" until it lands, then wait for the process to exit. Never kill it // on our own: a node that is busy (a witness rebuild can hold it for an hour) or flushing would lose // everything since its last save and replay it on the next start. Only an explicit user "Kill", // offered once the node has gone idle, ends it by force; "Leave running" lets the wallet close while // the node finishes on its own. // ------------------------------------------------------------------------------------------------ using StopUserChoice = daemon::StopUserChoice; using StopOutcome = daemon::StopOutcome; struct StopPolicy { bool owned = true; // we launched it: watch the process, signal it, offer Kill bool gracefulSignal = false; // POSIX: a group SIGTERM is a stop channel (dragonxd treats it as stop) double maxWaitSec = 0.0; // stop waiting (never kill) after this long; 0 = no limit bool userChoices = false; // the shutdown screen offers "Leave running" / "Kill" 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 }; // What the stop loop reports for display. struct StopStatus { bool delivered = false; // stop accepted (or the node is shutting down, or was signalled) bool heard = false; // the node has the request (possibly still queued) bool signalled = false; // a graceful signal (POSIX SIGTERM) went out double elapsedSec = 0.0; double cpuPercent = -1.0; // < 0: unknown double idleSec = 0.0; bool killOffered = false; bool rebuildActive = false; StopSendResult lastSend = StopSendResult::Other; int attempts = 0; }; class StopRuntime { public: virtual ~StopRuntime() = default; virtual double nowSec() = 0; virtual bool processAlive() = 0; virtual StopSendResult trySendStop() = 0; virtual bool nextCredentials() = 0; // switch to the next stop credentials; false when none left virtual bool sendGracefulSignal() = 0; // POSIX group SIGTERM; false where there is none (Windows) virtual double cpuSeconds() = 0; // cumulative CPU of the node; < 0 if unknown virtual double ioBytes() = 0; // cumulative I/O bytes of the node; < 0 if unknown virtual bool rebuildActive() = 0; // a witness rebuild is running: never offer Kill virtual StopUserChoice userChoice() = 0; virtual void offerForceKill(bool offered) = 0; virtual void forceKill() = 0; virtual void setStatus(const StopStatus& status) = 0; }; /** * "Making progress" = >= 0.05 cores of CPU or >= 1 MB of I/O over the last >= 60s. A flush that * fsyncs a big wallet is mostly I/O at low CPU; a rebuild is mostly CPU. Idle time counts from the * first sample. With no readings at all (unknown), it never reports idle, so Kill is never offered. */ class StopProgressTracker { public: static constexpr double kWindowSec = 60.0; static constexpr double kMinCores = 0.05; static constexpr double kMinIoBytes = 1024.0 * 1024.0; void add(double t, double cpuSec, double ioBytes) { if (cpuSec >= 0 || ioBytes >= 0) measurable_ = true; if (samples_.empty()) lastProgressAt_ = t; samples_.push_back({t, cpuSec, ioBytes}); // Keep the newest sample that is at least a full window old as the comparison base. while (samples_.size() > 2 && samples_[1].t <= t - kWindowSec) samples_.pop_front(); const Sample& base = samples_.front(); const double span = t - base.t; if (span >= kWindowSec) { const bool cpuMoved = cpuSec >= 0 && base.cpu >= 0 && (cpuSec - base.cpu) / span >= kMinCores; const bool ioMoved = ioBytes >= 0 && base.io >= 0 && (ioBytes - base.io) >= kMinIoBytes; if (cpuMoved || ioMoved) lastProgressAt_ = t; } } double idleSec(double t) const { return measurable_ ? std::max(0.0, t - lastProgressAt_) : 0.0; } // CPU over the most recent sample interval (100 = one core), for display; < 0 unknown. double cpuPercent() const { if (samples_.size() < 2) return -1.0; const Sample& a = samples_[samples_.size() - 2]; const Sample& b = samples_.back(); if (a.cpu < 0 || b.cpu < 0 || b.t <= a.t) return -1.0; return std::max(0.0, (b.cpu - a.cpu) / (b.t - a.t) * 100.0); } private: struct Sample { double t, cpu, io; }; std::deque samples_; double lastProgressAt_ = 0.0; bool measurable_ = false; }; enum class StopWaitDecision { Done, Wait, GiveUp, Detach, OfferForceKill, ForceKill }; struct StopWaitSample { bool processAlive = true; bool delivered = false; double elapsedSec = 0.0; double idleSec = 0.0; bool rebuildActive = false; StopUserChoice choice = StopUserChoice::None; bool killOffered = false; // the Kill offer is on screen right now bool shuttingDown = false; // the app began shutting down / the task was cancelled }; static inline StopWaitDecision evaluateStopWait(const StopPolicy& p, const StopWaitSample& s) { if (!s.processAlive) return StopWaitDecision::Done; if (p.userChoices && s.choice == StopUserChoice::Detach) return StopWaitDecision::Detach; if (p.userChoices && p.owned && s.choice == StopUserChoice::Kill && s.killOffered) return StopWaitDecision::ForceKill; if (p.abandonOnShutdown && s.shuttingDown) return StopWaitDecision::GiveUp; if (p.maxWaitSec > 0 && s.elapsedSec >= p.maxWaitSec) return StopWaitDecision::GiveUp; if (p.userChoices && p.owned && s.delivered && !s.rebuildActive && s.idleSec >= p.idleOfferSec) return StopWaitDecision::OfferForceKill; return StopWaitDecision::Wait; } static constexpr int kStopTickMs = 250; static inline StopOutcome executeDaemonStop(const StopPolicy& policy, StopRuntime& rt, LifecycleTaskContext& task) { if (!rt.processAlive()) return StopOutcome::Exited; // already gone: not a single RPC call const double t0 = rt.nowSec(); StopDeliveryPolicy delivery(policy.owned); StopProgressTracker progress; bool signalled = false; bool offered = false; auto withdrawOffer = [&] { if (offered) { offered = false; rt.offerForceKill(false); } }; for (;;) { double now = rt.nowSec() - t0; if (delivery.shouldSend(now)) { const StopSendResult r = delivery.record(rt.trySendStop(), now); if (r == StopSendResult::Auth && policy.owned) { // Our own node refusing our credentials means they're stale (e.g. a regenerated // .cookie): try the next source; out of sources, the graceful signal below remains. if (rt.nextCredentials()) delivery.retryNow(rt.nowSec() - t0); else delivery.giveUp(); } // POSIX: a group SIGTERM bypasses the RPC queue altogether (dragonxd's handler sets the same // flag as stop). Send it as soon as an attempt shows stop didn't get through. if (policy.gracefulSignal && !signalled && !delivery.delivered() && r != StopSendResult::Queued && rt.sendGracefulSignal()) { signalled = true; delivery.markDelivered(); } } if (!rt.processAlive()) { withdrawOffer(); return StopOutcome::Exited; } now = rt.nowSec() - t0; progress.add(now, rt.cpuSeconds(), rt.ioBytes()); StopWaitSample s; s.delivered = delivery.delivered(); s.elapsedSec = now; s.idleSec = progress.idleSec(now); s.rebuildActive = rt.rebuildActive(); s.choice = rt.userChoice(); s.killOffered = offered; s.shuttingDown = task.cancelled() || task.shuttingDown(); switch (evaluateStopWait(policy, s)) { case StopWaitDecision::Done: withdrawOffer(); return StopOutcome::Exited; case StopWaitDecision::Detach: withdrawOffer(); return StopOutcome::Detached; case StopWaitDecision::ForceKill: withdrawOffer(); rt.forceKill(); return StopOutcome::Killed; case StopWaitDecision::GiveUp: withdrawOffer(); return StopOutcome::StillRunning; case StopWaitDecision::OfferForceKill: if (!offered) { offered = true; rt.offerForceKill(true); } break; case StopWaitDecision::Wait: withdrawOffer(); // progress resumed (or a rebuild started): the node isn't stuck after all break; } StopStatus st; st.delivered = delivery.delivered(); st.heard = delivery.heard(); st.signalled = signalled; st.elapsedSec = now; st.cpuPercent = progress.cpuPercent(); st.idleSec = s.idleSec; st.killOffered = offered; st.rebuildActive = s.rebuildActive; st.lastSend = delivery.last(); st.attempts = delivery.attempts(); rt.setStatus(st); task.sleepForMs(kStopTickMs); } } private: std::unique_ptr daemon_; }; } // namespace daemon } // namespace dragonx