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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//! `epicsExit` — the process's one shutdown owner.
//!
//! This is C `libCom/misc/epicsExit.c`: a process-wide list of callbacks, and
//! the one call that runs them. `softIoc`'s `main` reaches every one of its
//! exits through `epicsExit(status)` (`softMain.cpp:167`, `:172`, `:251`,
//! `:265`, `:270`, `:277`), and `epicsExit` runs the list before handing the
//! status to `exit()` (`epicsExit.c:172-177`). Anything with teardown to do —
//! a driver that owes its device a goodbye frame, a file that must be flushed,
//! a thread that must be joined — registers here and stays ignorant of when
//! shutdown happens or who triggered it.
//!
//! Without it every such teardown is written but never reached: the fix
//! compiles, the `Drop` is correct, and the process exits around it. That was
//! the shape of the defect this module closes — an MQTT driver whose `Drop`
//! sends DISCONNECT while nothing at IOC exit ever dropped the driver.
//!
//! # Semantics, all of them C's
//!
//! * **LIFO.** [`call_at_exits`] pops from the tail (`ellLast`,
//! `epicsExit.c:88`), so a subsystem is torn down before whatever it was
//! built on.
//! * **Once.** It takes the list out of the static before running it
//! (`pExitPvtPerProcess = 0` under the lock, `:104-108`), so a second call —
//! or a concurrent one — runs nothing. Each callback is therefore
//! `FnOnce`, which is what C's "call, unlink, free" (`:93-95`) amounts to.
//! * **Unlocked while running.** C releases `exitPvtLock` before calling a
//! single callback (`:106-113`), which is what lets a callback register
//! another one, or call [`call_at_exits`] itself, without deadlocking.
//! * **Never removed.** C has no `epicsRemoveAtExit`; a registration lasts for
//! the process. A caller whose subject may already be gone by exit time
//! registers something that copes with that, as asyn's `destroyPortDriver`
//! does by looking the port up by name (`asynManager.c:2026-2043`).
//!
//! C's per-thread half (`epicsAtThreadExit`, `epicsExitCallAtThreadExits`) has
//! no caller here and is not ported: Rust's thread-local `Drop` already runs
//! at thread exit, which is the service that half exists to provide.
use Mutex;
/// One registered callback, with the name that identifies it in diagnostics —
/// C's `exitNode` (`epicsExit.c:38-43`), whose `name[]` exists for the same
/// reason (`atExit %s(%p)`, `:90`).
/// C's `pExitPvtPerProcess` (`epicsExit.c:53`) behind its `exitPvtLock`
/// (`:54`). `Vec` rather than a list because the only two operations are
/// "append" and "drain from the tail".
static EXIT_LIST: = new;
/// Register `func` to run at process shutdown — C `epicsAtExit3`
/// (`epicsExit.c:158-171`).
///
/// `name` is the diagnostic label C carries in the node; give it the subject,
/// not the verb (`"asynPort SERIAL1"`, not `"close the port"`), because it is
/// what a wedged shutdown is reported against.
///
/// Registrations are never removed, so a callback must be safe to run against a
/// subject that has since gone away.
/// Run every registered callback, most recent first, and clear the list — C
/// `epicsExitCallAtExits` (`epicsExit.c:100-115`).
///
/// The list is taken under the lock and released before the first callback
/// runs, so a callback may register another (it will not run) or call this
/// again (it will find nothing). Calling it a second time is a no-op, which is
/// what makes it safe to put on more than one exit path.
///
/// This is the IOC's shutdown, not the process's `exit()`: it returns, and the
/// caller decides what happens next. [`exit`] is the pairing C's `main` uses.
/// Run the exit callbacks and end the process — C `epicsExit`
/// (`epicsExit.c:172-177`).
///
/// The pause before `exit()` is C's `epicsThreadSleep(0.1)` (`:175`): a
/// callback that asked a thread to stop has, by then, only asked. Nothing here
/// joins those threads, so the pause is all the grace they get — a callback
/// that needs its subject actually gone must wait for it itself.
!