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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
//! Implementation of the DServer class.
use tracing::{debug, warn};
use crate::error::{reason, Error, ErrorKind, Result};
use crate::value::{CommandDataKind, Scalar, Spectrum};
use super::def::{ClassDef, CommandDef};
use super::server::Server;
use super::dev::Dev;
/// Class name of the admin class: DServer.
pub const CLASS_NAME: &str = "DServer";
/// The server administration model, this model is static and independant, and is just a
/// simple class implemented like a regular device class.
///
/// The admin device is the device we are using to boot up the server and provide admin
/// functions.
pub struct Admin {
}
impl Admin {
/// The definition of the admin class, which is the whole administration interface of
/// a Tango server: every command below is one a client, `Astor` and `Jive` in
/// particular, expects to find on `dserver/exe/inst` and nowhere else.
///
/// The commands every device has, `State`, `Status` and `Init`, are not among them:
/// the server answers those itself, on the admin device as on any other.
///
/// TODO: Only the commands of the server itself are implemented: the others are
/// declared with the signature a client expects and answer that they have no execution
/// function until they are written.
///
/// TODO: The commands of cppTango that no specification has, and that are therefore
/// left out until they are wanted: `QueryEventSystem`, `EnableEventSystemPerfMon` and
/// the whole `*Telemetry*` family, all of them from cppTango 10.
pub fn def() -> ClassDef<Self> {
ClassDef::new_async(CLASS_NAME, Self::init)
.doc("The administration device of that server, one per server, through which \
its devices are restarted, polled, logged and subscribed to.")
// --- The server itself --- //
.command(CommandDef::new("DevRestart")
.in_doc("Device name")
.exec_async(Self::dev_restart))
.command(CommandDef::new("RestartServer")
.exec_async(Self::restart_server))
.command(CommandDef::new("QueryClass")
.out_doc("Device server class(es) list")
.exec(Self::query_class))
.command(CommandDef::new("QueryDevice")
.out_doc("Device server device(s) list")
.exec(Self::query_device))
.command(command("QuerySubDevice", CommandDataKind::Void, CommandDataKind::StringArray)
.out_doc("Device server sub device(s) list"))
.command(CommandDef::new("Kill")
.exec_async(Self::kill))
// --- Polling --- //
.command(command("PolledDevice", CommandDataKind::Void, CommandDataKind::StringArray)
.out_doc("Polled device name list"))
.command(command("DevPollStatus", CommandDataKind::String, CommandDataKind::StringArray)
.in_doc("Device name")
.out_doc("Device polling status"))
.command(command("AddObjPolling", CommandDataKind::LongStringArray, CommandDataKind::Void)
.in_doc("Long[0]=Upd period. Str[0]=Device name. Str[1]=Object type. \
Str[2]=Object name"))
.command(command("UpdObjPollingPeriod", CommandDataKind::LongStringArray, CommandDataKind::Void)
.in_doc("Long[0]=Upd period. Str[0]=Device name. Str[1]=Object type. \
Str[2]=Object name"))
.command(command("RemObjPolling", CommandDataKind::StringArray, CommandDataKind::Void)
.in_doc("Str[0]=Device name. Str[1]=Object type. Str[2]=Object name"))
.command(command("StartPolling", CommandDataKind::Void, CommandDataKind::Void))
.command(command("StopPolling", CommandDataKind::Void, CommandDataKind::Void))
// --- Logging --- //
.command(command("AddLoggingTarget", CommandDataKind::StringArray, CommandDataKind::Void)
.in_doc("Str[i]=Device-name. Str[i+1]=Target-type::Target-name"))
.command(command("RemoveLoggingTarget", CommandDataKind::StringArray, CommandDataKind::Void)
.in_doc("Str[i]=Device-name. Str[i+1]=Target-type::Target-name"))
.command(command("GetLoggingTarget", CommandDataKind::String, CommandDataKind::StringArray)
.in_doc("Device name")
.out_doc("Logging target list"))
.command(command("SetLoggingLevel", CommandDataKind::LongStringArray, CommandDataKind::Void)
.in_doc("Long[i]=Logging Level. Str[i]=Device name."))
.command(command("GetLoggingLevel", CommandDataKind::StringArray, CommandDataKind::LongStringArray)
.in_doc("Device list")
.out_doc("Long[i]=Logging Level. Str[i]=Device name."))
.command(command("StartLogging", CommandDataKind::Void, CommandDataKind::Void))
.command(command("StopLogging", CommandDataKind::Void, CommandDataKind::Void))
// --- Events --- //
.command(command("EventSubscriptionChange", CommandDataKind::StringArray, CommandDataKind::Long)
.in_doc("Event consumer wants to subscribe to")
.out_doc("Tango lib release"))
.command(CommandDef::new("ZmqEventSubscriptionChange")
.exec_async(Self::zmq_event_subscription_change)
.in_doc("Event consumer wants to subscribe to.\n\
device name, attribute/pipe name, action (\"subscribe\"), event name, \
<Tango client IDL version>\n\
event name can take the following values:\n\
\x20 \"change\",\n\
\x20 \"alarm\",\n\
\x20 \"periodic\",\n\
\x20 \"archive\",\n\
\x20 \"user_event\",\n\
\x20 \"attr_conf\",\n\
\x20 \"data_ready\",\n\
\x20 \"intr_change\",\n\
\x20 \"pipe\"\n\
\"info\" can also be used as single parameter to retrieve information \
about the heartbeat and event pub endpoints.")
.out_doc("Str[0] = Heartbeat pub endpoint - Str[1] = Event pub endpoint\n\
...\n\
Str[n] = Alternate Heartbeat pub endpoint - Str[n+1] = Alternate Event \
pub endpoint\n\
Str[n+1] = event name used by this server as zmq topic to send events\n\
Str[n+2] = channel name used by this server to send heartbeat events\n\
Long[0] = Tango lib release - Long[1] = Device IDL release\n\
Long[2] = Subscriber HWM - Long[3] = Multicast rate\n\
Long[4] = Multicast IVL - Long[5] = ZMQ release"))
.command(CommandDef::new("EventConfirmSubscription")
.exec_async(Self::event_confirm_subscription)
.in_doc("Str[0] = dev1 name, Str[1] = att1 name, Str[2] = event name, \
Str[3] = dev2 name, Str[4] = att2 name, Str[5] = event name,..."))
// Only a server running without a database answers this itself; every other
// one has its channel in the database for a client to read there.
.command(command("QueryEventChannelIOR", CommandDataKind::Void, CommandDataKind::String)
.out_doc("Device server event channel IOR"))
// --- Locking --- //
.command(command("LockDevice", CommandDataKind::LongStringArray, CommandDataKind::Void)
.in_doc("Str[0] = Device name. Long[0] = Lock validity"))
.command(command("UnLockDevice", CommandDataKind::LongStringArray, CommandDataKind::Long)
.in_doc("Str[x] = Device name(s). Long[0] = Force flag")
.out_doc("Device global lock counter"))
.command(command("ReLockDevices", CommandDataKind::StringArray, CommandDataKind::Void)
.in_doc("Device(s) name"))
.command(command("DevLockStatus", CommandDataKind::String, CommandDataKind::LongStringArray)
.in_doc("Device name")
.out_doc("Device locking status"))
// --- What a wizard asks a class of --- //
.command(command("QueryWizardClassProperty", CommandDataKind::String, CommandDataKind::StringArray)
.in_doc("Class name")
.out_doc("Class property list (name - description and default value)"))
.command(command("QueryWizardDevProperty", CommandDataKind::String, CommandDataKind::StringArray)
.in_doc("Class name")
.out_doc("Device property list (name - description and default value)"))
}
/// The initialization of the admin device is a bit special since it is itself
/// reponsible for querying the database, if any, to find all devices and their
/// classes, and then dynamically add the devices to the server in order to boot
/// them.
///
/// The devices added here are not initialized by this function: they are new devices
/// of the server, and the synchronization that initialized this admin device picks
/// them up in its next batch, see [`super::server::Server::sync`]. So the whole boot of a
/// server is the initialization of its admin device, and the admin device is the only
/// device a server needs to be given.
async fn init(dev: &Dev<Self>) -> Result<Self> {
// Failing here fails the admin device itself, which is the right place for it:
// without that list the server has nothing to serve, and a client asking the admin
// device what is wrong is told exactly that.
add_listed_devs(dev.server()).await?;
Ok(Self {})
}
/// Stop the server, see [`super::server::Server::stop`]: this request is still
/// answered, and the thread waiting on the server then deletes its devices.
async fn kill(&self, dev: &Dev<Self>, _: ()) -> Result<()> {
dev.server().stop().await
}
/// Restart a device of this server: deleted and initialized again, unexported and
/// exported again, which is what the device gets from its class anew, see
/// [`Server::sync`]. The admin device is not among them, see
/// [`Self::restart_server`].
///
/// The name may be given as a client knows it, with the database or the protocol in
/// front of it, and options behind it.
async fn dev_restart(&self, dev: &Dev<Self>, name: Scalar<str>) -> Result<()> {
let server = dev.server();
let name = local_dev_name(name.value());
let target = server.get_dev(name)
.filter(|target| !target.class().name().eq_ignore_ascii_case(CLASS_NAME))
.ok_or_else(|| Error::new(ErrorKind::Reason(reason::API_DeviceNotFound))
.with_description(format!("Device {name} not found")))?;
let (name, class) = (target.name().to_string(), target.class().name().to_string());
drop(target);
server.remove_dev(&name);
server.add_dev(&class, &name)?;
server.sync().await
}
/// Restart every device of this server but the admin one: all removed, then added
/// again from what the database lists now, which is how a server picks up the devices
/// added to it or removed from it in the database.
///
/// The request is answered once the devices are restarted, which a client waiting for
/// the restart to be done can rely on, so it takes as long as initializing every device
/// does.
async fn restart_server(&self, dev: &Dev<Self>, _: ()) -> Result<()> {
let server = dev.server();
for dev in server.devs() {
if !dev.class().name().eq_ignore_ascii_case(CLASS_NAME) {
server.remove_dev(dev.name());
}
}
add_listed_devs(server).await?;
server.sync().await
}
/// Have an event published, which is entirely up to the backend serving this server,
/// see [`crate::backend::BackendServer::zmq_event_subscription_change`].
async fn zmq_event_subscription_change(&self, dev: &Dev<Self>, args: Spectrum<str>) -> Result<(Spectrum<i32>, Spectrum<str>)> {
dev.server().served_backend()?.zmq_event_subscription_change(args).await
}
/// Keep the events subscribed to published, which is entirely up to the backend
/// serving this server, see [`crate::backend::BackendServer::event_confirm_subscription`].
async fn event_confirm_subscription(&self, dev: &Dev<Self>, args: Spectrum<str>) -> Result<()> {
dev.server().served_backend()?.event_confirm_subscription(args).await
}
/// The classes of this server, the admin one aside, in the order they were made.
fn query_class(&self, dev: &Dev<Self>, _: ()) -> Result<Vec<String>> {
Ok(dev.server().classes().iter()
.filter(|class| !class.name().eq_ignore_ascii_case(CLASS_NAME))
.map(|class| class.name().to_string())
.collect())
}
/// The devices of this server, the admin one aside, each as `Class::name`, class by
/// class.
fn query_device(&self, dev: &Dev<Self>, _: ()) -> Result<Vec<String>> {
Ok(dev.server().devs().iter()
.filter(|dev| !dev.class().name().eq_ignore_ascii_case(CLASS_NAME))
.map(|dev| format!("{}::{}", dev.class().name(), dev.name()))
.collect())
}
}
/// The name of a device of this server out of a name as a client gives it: what is behind
/// a `#`, the options of the name, is dropped, and so is what is in front of the name for
/// a full one, `tango://host:port/` or the protocol it is reached with.
fn local_dev_name(name: &str) -> &str {
let name = name.split_once('#').map_or(name, |(name, _)| name);
match name.split_once("://") {
Some((_, rest)) => rest.split_once('/').map_or(rest, |(_, name)| name),
None => name,
}
}
/// Add to that server the devices the database lists for it, each with the class it is an
/// instance of, the admin device aside: they are initialized and exported by the next
/// synchronization, see [`Server::sync`].
async fn add_listed_devs(server: &Server) -> Result<()> {
let listed = server.database().list_server_dev_async(server.name()).await?;
for dev in &listed {
let (name, class) = (dev.name.value(), dev.value.value());
// The admin device is itself registered in the database, as a device of the
// DServer class of this server, and it is the very device running this.
if class.eq_ignore_ascii_case(CLASS_NAME) {
continue;
}
// A device of a class this executable does not define, or a device the server
// already has, is a misconfiguration of that device alone: the others are
// still added and the server still boots, which is what makes a server usable
// while one of its devices is being fixed.
if let Err(err) = server.add_dev(class, name) {
warn!(dev = %name, %class, %err, "Failed to add the device listed by the database");
continue;
}
debug!(dev = %name, %class, "Added the device listed by the database");
}
Ok(())
}
/// A command of the administration interface, which is a name and the two kinds it carries:
/// none of them is implemented, so nothing derives those kinds and each has to be given.
fn command(name: &'static str, in_kind: CommandDataKind, out_kind: CommandDataKind) -> CommandDef<Admin> {
CommandDef::new(name)
.in_kind(in_kind)
.out_kind(out_kind)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_names() {
assert_eq!(local_dev_name("a/b/c"), "a/b/c");
assert_eq!(local_dev_name("a/b/c#dbase=no"), "a/b/c");
assert_eq!(local_dev_name("tango://host:10000/a/b/c#dbase=no"), "a/b/c");
}
}