1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//! The restart-trigger admin channel: a small, local-only listener
//! (`tokio::net::UnixListener` on Unix, a named pipe on Windows) an
//! already-running process listens on so `xr restart` (or any other local
//! caller) can ask it to perform a zero-downtime restart handoff.
//!
//! Preferred over a loopback TCP port: OS-level file/pipe permissions
//! give real access control for free (no auth-token scheme needed for a
//! first version), there's no risk of colliding with the app's own
//! `app_port` or another local service, and it doesn't show up as an open
//! network port to scan. The path/name is derived deterministically from
//! `Config::app_name` - both `Application::serve()` and `xr restart`
//! compute it identically, independently, with no runtime negotiation
//! needed to agree on where to find it.
use TcpListener as StdTcpListener;
use Duration;
pub const RESTART_COMMAND: &str = "RESTART";
/// Asks the running process to shut down gracefully with no replacement
/// spawned at all - for a caller that no longer holds a `Child` handle to
/// whatever's currently serving (e.g. `xr dev`, once it's handed off past
/// the first generation) and needs a reliable way to reach "whoever is
/// currently listening" for a clean teardown. OS signals don't help here
/// on Windows specifically: `signal.rs`'s own reasoning already
/// establishes that `GenerateConsoleCtrlEvent(CTRL_C_EVENT)` can't target
/// one specific process, and `CTRL_BREAK_EVENT` only works for a child
/// spawned with `CREATE_NEW_PROCESS_GROUP` - not the case for a
/// generation the caller never itself spawned. The admin channel, being
/// address-based rather than pid-based, is the one mechanism that
/// already reaches "whoever owns this address right now" regardless of
/// spawn lineage.
pub const STOP_COMMAND: &str = "STOP";
/// Asks the running process to push a `reload-assets` SSE event to every
/// connected dev-reload client (see `crate::dev_reload::broadcast_asset_reload`)
/// without restarting anything - `xr dev`'s response to a change confined
/// to `public/`, where nothing needs recompiling and a full handoff would
/// just be slower for no benefit. Unlike `RESTART`/`STOP`, handling this
/// never ends the admin loop: the process receiving it keeps right on
/// serving, so it must stay ready for the next command.
pub const RELOAD_ASSETS_COMMAND: &str = "RELOAD_ASSETS";
/// Pushes a `build-status` SSE event to every connected dev-reload client
/// (`crate::dev_reload::broadcast_build_status`) without restarting
/// anything - `xr dev`'s signal that a rebuild has started or just failed,
/// for every rebuild *after* the very first one (the placeholder page
/// covers that one on its own, with no admin channel involved at all).
/// The wire form is `"BUILD_STATUS <status>"`, one space-separated word -
/// `xr dev` only ever sends `building`/`failed`, but the payload is
/// forwarded to the browser verbatim rather than re-validated here; see
/// `dev_reload`'s own module doc comment for why the *old*, still-good
/// process serving throughout a rebuild is exactly the gap this closes.
/// Like `RELOAD_ASSETS`, never ends the admin loop.
pub const BUILD_STATUS_COMMAND: &str = "BUILD_STATUS";
pub const ACK_HANDOFF_STARTED: &str = "OK";
pub const ACK_HANDOFF_FAILED: &str = "FAILED";
/// What the admin channel loop resolved to - either a restart handoff
/// actually succeeded (`Handoff`, carrying the new, already-serving
/// child), or a plain `STOP` was received (no replacement spawned at
/// all). The caller (`Application::serve()`) treats both the same way
/// from here on: begin its own graceful shutdown.
/// Deterministic per-app admin-channel address - a plain identifier, not
/// a full path/pipe-name; each platform's own module turns it into the
/// concrete form it needs (a socket file path on Unix, a
/// `\\.\pipe\...` name on Windows).
/// Runs the admin channel loop until either a restart handoff actually
/// succeeds or a plain `STOP` is received - either way, the caller
/// (`Application::serve()`) treats the result as "begin my own graceful
/// shutdown now" (see `AdminOutcome`). A *failed* handoff attempt (the
/// spawned replacement crashed, or never reported ready within
/// `ready_timeout`) is reported back to whoever asked, and this function
/// keeps looping, ready to accept another attempt later - a bad build
/// doesn't wedge the currently-running, still-healthy process out of
/// ever restarting again.
///
/// Deliberately does **not** take a `binary_path` parameter - each
/// platform's own implementation resolves `handoff::resolve_binary_path()`
/// fresh, at the moment a `RESTART` is actually received, not once up
/// front. A `storage/releases/current` pointer written *after* this
/// process booted but *before* a `RESTART` arrives must be respected;
/// resolving once at boot (the original, buggy shape of this function)
/// silently ignored any such update. See `docs/GOTCHAS.md`.
pub async