-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaioenetd.cpp
More file actions
1779 lines (1532 loc) · 60.9 KB
/
Copy pathaioenetd.cpp
File metadata and controls
1779 lines (1532 loc) · 60.9 KB
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
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// aioenetd.cpp
// Main source file for aioenetd, the systemd service/application
// that listens to some TCP ports and provides the packet-level interface to eNET- devices
/*
when a connection to ~8080 (the "control" connection port) occurs send a Hello 'H' TMessage with several TDataItems.
At least one data item is the ConnectionID, others may include PID, Model#, Serial#, what the onboard RTC thinks the time is, etc
DId 0x7001 "TCP_ConnectionID", 4 byte ConnectionID is the data
When a connection to ~8080+1 (the ADC Streaming port) occurs, send (__u32)(0x80000000|ConnectionID) ("Invalid ADC data value bit set + ConnectionID").
Connection IDs are opaque daemon-issued tokens, not Linux socket descriptors.
ADC_StreamStart(ConnectionID) resolves the ADC token to a still-connected data socket.
*/
/*[aioenetd Protocol 2 TCP-Listener/Server Daemon/Service implementation and concept notes]
from discord code-review conversation with Daria; these do not belong in this source file:
your "the main loop" == "my worker thread that dequeues Actions";
your "exploding" == "my Object factory construction";
You've moved "my Object construction" into a single-threaded spot, "the main loop", from where I have it, in an
individual socket's receive-thread.
Because I have multiple receive threads it isn't nearly as necessary to "be fast": the TCP Stack will queue bytes for me.
Because my error-checking location (the parser, the exploder, .FromBytes()) is in a socket-specific (ie client-specific)
thread it is harde\
{ \
; \
}
Because my worker and all receive threads share a thread-safe std::queue<> everything is serialized nicely.
By putting 66% of the error checking, syntax AND semantic (but not operational errors, eg hardware timeouts) into the
receive threads — and in fact, into the TMessage constructor, I am guaranteed all TMessage Objects are valid and safe
to submit to the worker thread, thus less likely to cause errors in that single-thread / single-point-of-failure
(and make the worker execution faster, as it is "my single thread" and thus bottleneck)
-----
A TMessage is constructed from received bytes by `auto aMessage = TMessage::fromBytes(buf);`, or an "X" Response TMessage with
syntax error details gets returned, instead.
Either way, the constructed TMessage is pushed into the Action Queue.
The asynchronous worker thread pops a TMessage off the Action Queue, does a `for(aDataItem : aMessage.Payload){aDataItem.Go()}`,
and either modifies-in-place and sends the TMessage as a reply or constructs and sends a new TMessage as the reply,
the TMessage(s) then goes out of scope and deallocates
-----
The TMessage library needs to handle syntax errors, mistakes in the *format* of a bytestream, and semantic errors, mistakes
in the *content* of the received bytes. Some categoriese of semantic errors, however, are specific to a particular model of eNET-
device, let alone specific to a model Family.
Consider ADC_GetChannelV(iChannel): iChannel is valid if (0 <= iChannel <= 15), right? Nope, not "generally": this is only true
for the ~12 models in the base Family, eNET-AIO16-16F, eNET-AI12-16E, etc. But eNET- devices, and therefore Protocol 2 devices
intended to operate via this TMessage library and the aioenetd implementation, include the DPK and DPK M Families; this means:
iChannel is valid if (0 <= iChannel <= highestAdcChan), and highestAdcChan is 15, 31, 63, 96, or 127; depending on the specific
model running aioenetd/using this library.
So, to catch Semantic errors of this type (invalid channel parameter in an ADC_GetChannelV() TDataItem) the parser must "know"
the value of "highestAdcChan" for the model it is running on.
The sum of all things that the parser needs to know to handle semantic error checking, specific to the model running the library,
are encapsulated as "getters" in a HAL. The list is quite long as there are a LOT of variations built out of the eNET-AIO design.
These "getters" *could* be implemented in a static, compile-time, manner. Consider a device_specific.h file that has a bunch of
eg `#define highestAdcChan 31`-type constants defined. However, this is a "write it for 1" approach, and would require loading a
different TMessage library binary for every model, as the binary is effectively hard-coded for a specific device's needs.
The USB FWE2.0 firmwares are almost this simplistic in their approach to a HAL: there is a device_specific.h, but there is also a
run-time operation that introspects the DeviceID and tweaks some constants, like the friendly_model_name string, to a model-specific
value. This run-time operation is a hard-coded switch(DeviceId){}, and thus is implemented per Firmware (but allows one Firmware
to run, as a binary, on any models built from the same or compatible PCB).
This only works because there are very few variants per firmware source: the USB designs are sufficiently different that reusing
firmware is implausible: it would require a *real* HAL implementation (one that abstracted every pin on the FX2, and every
register that could ever exist on the external address/data bus).
We're going for a better approach to the HAL in this library.
TBD, LOL
The library implementation, today (2022-07-21 @ 10:55am Pacific), only checks for Semantic errors in the one DId that's been
implemented; REG_Read1(offset). This DId uses a helper function hardcoded in the source called `WidthFromOffset(offset)`,
which returns one of 0, 8, or 32, indicating "invalid offset", "offset is a valid 8-bit register", or "offset is a valid
32-bit register", respectively. In theory it should be able to respond "16" as well, but the eNET-AIO, in all of its models,
has no 16-bit registers. This is an instance where the HAL is weak; "WidthFromOffset()" shouldn't be hard-coded; it should
be able to respond accurately about whatever device it is running on (or perhaps even whatever device it is asked about).
One way to accomplish this would be loading configuration information tables from nonvolatile on-device storage. "tables",
here, is plural not to refer to both a hypothetical single table needed for WidthFromOffset() and all the other tables for
supporting other HAL-queriables, but to express that WidthFromOffset() *alone* needs several tables, if it is to support
the general case. Sure, eNET-AIO only has registers that can each only be correctly accessed at a specific bit width (ie it is
unsafe or impossible to successfully read or write 8 bits from any eNET-AIO 32-bit register), but many ACCES devices do not
have this limitation; most devices support 8, 16, or 32-bit access to any register or group thereof (as long as offsets are
width-aligned; eg 32-bit operations require that offset % 4 == 0). WidthFromOffset() therefore becomes complex, and
declaratively describing each device's capabilities and restrictions is also complex.
Another approach would be to implement a "HAL interface" (C++ calls an interface an Abstract Base Class or ABC) which the library
would use to access the needed polymorphisms via a device-specific TDeviceHAL object it is provided ("Dependency Injection").
Every TDeviceHAL descendant would provide not-less-than a set of device-specific constants like `highestAdcChan` from the
introduction example. Better would be to also include "verbs" that implement generic operations as needed for the specific
device, like an ADC_SetRange1(iChannel, rangeSpan), but this level of interface is incredibly complex, in any generic form.
Consider the RA1216 which expects the ADC input range to be specified as a voltage span code plus an offset in ±16-bit counts;
This is an extreme example, an outlier; an example that forces "the most general case" to be handled ... but this is merely the
outlier in the "ADC RangeCode" axis. Consider the RAD242 which has a 24-bit ADC, the AD8-16 which has an 8-bit ADC, the
USB-DIO-32I which has 1 bit per I/O Group instead of the typical 8 or 4 bits — there are *many* axes of outliers, and they all
become necessary to handle if you try to make a truly generic library/HAL interface.
This is why "Universal Libraries" (like NI produces) are so big and difficult to code against.
Thus, "Protocol 2" is designed to be slightly generic, but more importantly, extensible. Devices running old Protocol libraries
should interoperate safely, politely, with Messages sent using new, extended, versions of the protocol.
[
during the writing of this I've determined the extensibility I'd designed into the protocol was insufficient to support an
already known "requirement" for extending TDataItem lengths beyond 127 bytes, and thinking about it I realized using the
most significant bit of a Length field as a sentinel to indicate an alternate syntax, or even one of a set, applies, does
not provide the ability to change the LENGTH field, unless that is defined in advance at day 1.
As a result of discussing this with Daria we've decided to just double the existing max payload lengths, in both TMessages
and TDataItems (i.e., moving from 16 to 32 bits, and from 8 to 16 bits, respectively). The delta overhead from this change
limits at 25%, and *bandwidth* isn't a concern (exceeding MTU-size multiples, thus increasing TCP packet count, is of some
concern).
No future version of this protocol will support longer length Messages. Instead, if an eNET- device *needs* longer
Messages or DataItems, or needs either in *unspecified* lengths, clients will connect to a different listen_port and use a different
protocol. This is how Protocol 1 supports "ADC Streaming".
]
[program overview]
Main spawns one action-thread for handling Protocol 2.
NOTE: Main might also spawn a singleton receive/action/send thread, or one set per "Streaming" type (ADC in the eNET-AIO case), to handle the streaming Protocol(s).
Each Client that connects spawns a receive-thread.
EITHER
1 the action-thread is responsible for sending data to the correct client
OR
2 each per-client receive-thread spawns a send-thread and queue which is stuffed from the action-thread
OR
3 a single send-thread and queue exist, created by Main and stuffed by the action-thread
Root listen in Main run-loop receives on primary connect listen_port#; valid connections spawn receive-threads that listen on the Socket
Multiple Clients can connect; each gets one listen-thread (and perhaps one send-queue & thread).
[receive-threads]
Each receive-thread passes received bytes in >= Message-sized chunks to TMessage::fromBytes to construct a TMessage instance; .fromBytes is a
class factory method that will construct the appropriate TDataItem descendants based on the TMessage.Payload bytes' DIds.
errors throw exceptions; the exception handler will programmatically construct a TMessage to report the detected Errors
Regardless, the receive-thread Queues the TMessage (either the one constructed via .fromBytes() or via the caught exception handler)
into the action-thread's Queue to be handled, and resumes waiting for additional messages.
The TMessage that was Queued is now owned by the Action Queue;
In the case of an error the TMessage that was received goes out of scope and is destroyed when the receive-thread run-loop loops.
[action-thread]
The action-thread run-loop pops a TMessage "ActionBundle" out of the Action Queue
If the Message is a report-error message it puts it into the send-queue and the run-loop loops.
NOTE: TMessages in the Action Queue are guaranteed to be "syntactically and semantically valid".
Normally it constructs a basic Reply TMessage. and proceeds.
It loops over the ActionBundle.TDataItems[] and calls .Go() on each.
Each .Go() performs its Action, using the derived-classes' DId-specific parameters as needed, and changes its internal state into a "resultDataItem"
Basically: once resultCode has been set (from .Go() the TDataItem .AsBytes() and .AsString() functions produce different results than before .Go(),
as they now include the resultCode and resultValue(s) as determined during .Go(). E.g., DIO_Read1() adds 1 or 0 to indicate the input level.
The send-thread then checks the .getResultCode(); if not ERR_SUCCESS it sets the Reply Message MId to reflect the operational error, 'E'.
Regardless, the send-thread then adds the modified TDataItem into the Reply Message. NOTE: TDataItems in TPayloads are actually shared_ptr<TDataItem>, so the
being-assembled Reply Message AND the ActionBundle's Message both hold a reference to the TDataItem at this point.
Once all TDataItems[] in the ActionBundle.Payload have been executed (.Go()), and stuffed into the Reply Message, .AsBytes() is called and the byte-buffer
produced is added to a SendBundle, which is then:
EITHER
1 sent to the correct Client across the socket, directly from the Action thread.
NOTE: this means ActionBundles need to hold a reference to the Client's Socket.
OR
2 added to a per-Client send-thread Queue to be sent asynchronously by the per-Client send-thread.
NOTE: this means ActionBundles need a reference to the Client's send-Queue.
OR
3 added to a single send-thread Queue to be sent asynchronously.
NOTE: this means ActionBundles AND SendBundles need a reference to the Client's Socket (the SendBundle just inherits it from the ActionBundle)
TBD: once I know more about TCP Sockets and such in Linux I can figure out which is best.
The send-thread run-loop has now completed one loop, so the TMessage it popped out of the Action Queue goes out of scope and is destroyed.
[1. no send-thread exists separate from the action-thread]
[2. one send-thread per Client]
When Main gets a Connection it spawns a listen-thread, which constructs a Send Queue then spawns a send-thread that will operate on the Send Queue.
[3. singleton send-thread]
Main constructs both the action-thread and send-thread; only one of each exist, each with one input Queue
The single Action Thread serves to serialize device operations, ensuring the Actions dictated in each received Message's payload get executed "atomically",
BUT the execution order is determined by the "parsing-finished" time, not by the "Message-received" time; i.e., each Client gets its turn in the order the
receive-threads' constructed TMessage gets added to the Action Queue.
The Main run-loop should act as a Watchdog, monitoring the various threads to ensure they haven't dead-locked.
All threads (including Main) should generate log file data, with programmatically configured verbosity level;
Logs should be retrievable via Protocol 2 Messages. (NYI)
*/
#include <algorithm>
#include <arpa/inet.h>
#include <cctype>
#include <cerrno>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <filesystem>
#include <netinet/in.h>
#include <poll.h>
#include <signal.h>
#include <sys/reboot.h>
#include <sys/wait.h>
#include <unistd.h>
#include "apci.h"
#include "build_info.h"
#include "daq_state.h"
#include "socket_util.h"
#include "logging.h"
#include "TMessage.h"
#include "adc.h"
#include "config.h"
#include "DataItems/ADC_.h"
#include "DataItems/BRD_.h"
#include "DataItems/CFG_.h"
#include "DataItems/DAC_.h"
#include "DataItems/REG_.h"
#include "DataItems/SYS_.h"
#include "DataItems/TDataItem.h"
#include "aioenetd.h"
#include "WebControl/webctl_aioenetd.h"
// #define MG_ARCH MG_ARCH_NEWLIB
// extern "C" {
// #include "mongoose.h"
// }
// Cross-module daemon reset request interface.
// Implemented in aioenetd.cpp; called by DataItems/BRD_.cpp.
enum class DaemonResetRequest : int {
None = 0,
Gentle = 1,
Force = 2
};
static volatile sig_atomic_t daemonResetRequest = static_cast<sig_atomic_t>(DaemonResetRequest::None);
static volatile sig_atomic_t forceRebootFallbackArmed = 0;
int apci = -1;
volatile sig_atomic_t done = 0;
TActionQueue ActionQueue;
static int ControlListenPort = 18767; // 0x494f, ASCII for "IO"
int AdcListenPort = ControlListenPort + 1;
pthread_t action_thread;
pthread_t controlListener_thread;
pthread_t adcListener_thread;
pthread_t webListener_thread;
static bool webListenerStarted = false;
enum class SysErrStage : __u32 { Parse = 1, Execute = 2 };
static inline __u32 ErrIndex(TError rc) { return -rc; }
// // Function to serve static files
// static void serve_static(struct mg_connection *nc, struct mg_http_message *hm) {
// struct mg_http_serve_opts opts = { .root_dir = "/home/acces/www" };
// mg_http_serve_dir(nc, hm, &opts);
// }
// // Function to handle API requests
// static void handle_api(struct mg_connection *nc, struct mg_http_message *hm) {
// if (mg_match(hm->uri, mg_str("/api/data*"), NULL) )
// {
// // Example response with dynamic data
// mg_http_reply(nc, 200, "Content-Type: application/json\r\n", "{\"adc\": [1.23, 2.34, 3.45]}");
// }
// else
// {
// mg_http_reply(nc, 404, "Content-Type: text/plain\r\n", "Not Found!");
// }
// }
// // Event handler for Mongoose
// static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
// struct mg_http_message *hm = (struct mg_http_message *) ev_data;
// switch (ev) {
// case MG_EV_HTTP_MSG:
// Log(std::string(hm->uri.buf, hm->uri.len));
// if (mg_match(hm->uri, mg_str("/api/*"), NULL) ) {
// handle_api(nc, hm);
// } else {
// serve_static(nc, hm);
// }
// break;
// default:
// break;
// }
// }
static void SetCloseOnExec(int fd, const char *what)
{
if (fd < 0) {
return;
}
int flags = fcntl(fd, F_GETFD);
if (flags < 0) {
Error(std::string("fcntl(F_GETFD) failed for ")
+ what + ": " + std::strerror(errno));
return;
}
if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) {
Error(std::string("fcntl(F_SETFD, FD_CLOEXEC) failed for ")
+ what + ": " + std::strerror(errno));
}
}
static void CloseFd(int &fd, const char *what)
{
if (fd < 0) {
return;
}
int oldFd = fd;
fd = -1;
if (close(oldFd) < 0) {
Error(std::string("close(") + what + ") failed for fd "
+ std::to_string(oldFd) + ": " + std::strerror(errno));
}
}
static std::string SystemStatusString(int status)
{
if (status == -1) {
return "system() failed: " + std::string(std::strerror(errno));
}
if (WIFEXITED(status)) {
return "exit=" + std::to_string(WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) {
return "signal=" + std::to_string(WTERMSIG(status));
}
return "raw status=" + std::to_string(status);
}
static bool EmergencySysRqReboot()
{
int fd = open("/proc/sysrq-trigger", O_WRONLY | O_CLOEXEC);
if (fd < 0) {
return false;
}
const char cmd = 'b'; // immediate reboot
const ssize_t wrote = write(fd, &cmd, 1);
close(fd);
return wrote == 1;
}
static void ForceRebootNow(const char *reason)
{
const char *why = reason ? reason : "unspecified reason";
Log(std::string("FORCE reboot requested: ") + why);
/*
First try the civilized path. --no-block asks systemd/init to start
rebooting but does not require this process to wait for the transaction.
*/
sync();
errno = 0;
int status = std::system(
"PATH=/usr/sbin:/usr/bin:/sbin:/bin; "
"systemctl --no-block reboot >/dev/null 2>&1"
);
if (status != 0) {
Error("FORCE reboot: systemctl --no-block reboot did not report success: "
+ SystemStatusString(status));
}
/*
Give systemd a brief chance to take over. If it does, this process will
usually be killed before reaching the harder fallbacks.
*/
sleep(5);
/*
Harder path: ask the kernel to reboot directly. This requires CAP_SYS_BOOT,
which aioenetd should normally have if it is running as the privileged
hardware daemon.
*/
sync();
errno = 0;
if (reboot(RB_AUTOBOOT) != 0) {
Error(std::string("FORCE reboot: reboot(RB_AUTOBOOT) failed: ")
+ std::strerror(errno));
}
/*
Last resort: emergency SysRq reboot. This intentionally bypasses normal
userspace shutdown. It belongs only in FORCE mode.
*/
if (!EmergencySysRqReboot()) {
Error("FORCE reboot: writing 'b' to /proc/sysrq-trigger failed");
}
/*
If we somehow got here, all reboot mechanisms failed. Do not return to
normal daemon execution after a FORCE reset request.
*/
_exit(127);
}
static void ArmForceRebootFallback()
{
if (forceRebootFallbackArmed) {
return;
}
forceRebootFallbackArmed = 1;
pid_t pid = fork();
if (pid < 0) {
Error(std::string("BRD_Reset(FORCE): failed to fork reboot fallback: ")
+ std::strerror(errno));
return;
}
if (pid == 0) {
/*
Child process. Keep this deliberately simple: this process exists
only as an out-of-band reboot hammer if the normal main()/exit_handler()
path stalls.
Do not Log(), malloc heavily, take locks, or call std::system() here.
The parent is multithreaded, so the forked child should stay minimal.
*/
setsid();
/*
Give the parent enough time to:
1. send the BRD_Reset reply,
2. leave ActionThread,
3. run exit_handler(),
4. reach ForceRebootNow().
*/
sleep(12);
/*
No sync() here by design. The normal path already syncs. This fallback
is for the case where normal shutdown is wedged, and sync() may wedge too.
*/
(void)reboot(RB_AUTOBOOT);
(void)EmergencySysRqReboot();
_exit(127);
}
Log("BRD_Reset(FORCE): armed out-of-process reboot fallback pid "
+ std::to_string(pid));
}
void RequestDaemonReset(DaemonResetRequest request)
{
if (request == DaemonResetRequest::Force) {
daemonResetRequest = static_cast<sig_atomic_t>(DaemonResetRequest::Force);
ArmForceRebootFallback();
}
else if (daemonResetRequest != static_cast<sig_atomic_t>(DaemonResetRequest::Force)) {
daemonResetRequest = static_cast<sig_atomic_t>(request);
}
done = 1;
}
DaemonResetRequest GetDaemonResetRequest()
{
return static_cast<DaemonResetRequest>(daemonResetRequest);
}
int main(int argc, char *argv[])
{
Intro(argc, argv);
InitConfig(Config);
InitializeConfigFiles(Config);
OpenDevFile(); // sets apci
try
{
LoadConfig();
}
catch (const std::logic_error &e)
{
Error(e.what());
};
ApplyConfig();
pthread_create(&action_thread, NULL, (void *(*)(void *)) & ActionThread, &ActionQueue);
pthread_create(&controlListener_thread, NULL, ControlListenerThread, (void *)AF_INET6);
pthread_create(&adcListener_thread, NULL, AdcListenerThread, (void *)AF_INET6);
if (WebCtlAioEnetd_Start(&webListener_thread) == 0)
webListenerStarted = true;
else
Warn("WebControl HTTP listener did not start; continuing without web dashboard");
// struct mg_mgr mgr;
// struct mg_connection *nc;
// mg_mgr_init(&mgr);
// nc = mg_http_listen(&mgr, "http://0.0.0.0:80", ev_handler, &mgr);
// if (nc == NULL) {
// printf("Failed to create listener\n");
// return 1;
// }
// printf("Starting server on port 80\n");
do
{
usleep(10000);
// mg_mgr_poll(&mgr, 1000);
} while (!done);
// mg_mgr_free(&mgr);
DaemonResetRequest resetRequest = GetDaemonResetRequest();
exit_handler(0);
if (resetRequest == DaemonResetRequest::Gentle) {
/*
BRD_Reset(gentle):
exit_handler() has already run, so threads are joined, APci is closed,
DAQ hardware was reset to safe state, and config was saved. Now restart
this daemon image in-place.
*/
execv("/proc/self/exe", argv);
Error(std::string("BRD_Reset(gentle): execv(/proc/self/exe) failed: ")
+ std::strerror(errno));
execv(argv[0], argv);
Error(std::string("BRD_Reset(gentle): execv(argv[0]) failed: ")
+ std::strerror(errno));
return 127;
}
else if (resetRequest == DaemonResetRequest::Force) {
ForceRebootNow("BRD_Reset(FORCE)");
}
return 0;
}
void abort_handler(int s)
{
done = 1;
}
void exit_handler(int s)
{
Log("exit process starting");
done = 1;
AdcShutdown();
if (DaqReady() && apci >= 0)
apci_cancel_irq(apci, 1);
// if (controlSocket >= 0)
// {
// shutdown(controlSocket, SHUT_RDWR);
// close(controlSocket);
// controlSocket = -1;
// }
// if (adcSocket >= 0)
// {
// shutdown(adcSocket, SHUT_RDWR);
// close(adcSocket);
// adcSocket = -1;
// }
WebCtlAioEnetd_Stop();
ActionQueue.enqueue(nullptr);
sleep(1);
if (webListenerStarted)
pthread_join(webListener_thread, NULL);
pthread_join(adcListener_thread, NULL);
pthread_join(controlListener_thread, NULL);
pthread_join(action_thread, NULL);
/* put an opened card back in the power-up state */
if (DaqReady() && apci >= 0)
{
out(ofsReset, bmResetEverything);
close(apci);
apci = -1;
}
SaveConfig();
// note __attribute__((unused)) is to silence an incorrect compiler warning
std::time_t end_time __attribute__((unused)) = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
Log(std::string("AIOeNET Daemon ") + BuildInfo::Version + " CLOSING, it is now: " + std::string(std::ctime(&end_time)));
}
void Intro(int argc, char **argv)
{
// stdout is captured by systemd. Line buffering makes each completed log
// record promptly available to journal readers even when stdout is not a TTY.
std::setvbuf(stdout, nullptr, _IOLBF, 0);
// note __attribute__((unused)) is to silence an incorrect compiler warning
const char *env = std::getenv("AIOENET_LOG_LEVEL");
if (env)
{
std::string lvl = env;
Log("AIOENET_LOG_LEVEL='" + lvl + "'");
std::transform(lvl.begin(), lvl.end(), lvl.begin(), ::tolower);
SetLogLevel(LogLevel::Error);
if (lvl == "trace")
SetLogLevel(LogLevel::Trace);
else if (lvl == "debug")
SetLogLevel(LogLevel::Debug);
else if (lvl == "info")
SetLogLevel(LogLevel::Info);
else if (lvl == "warning" || lvl == "warn")
SetLogLevel(LogLevel::Warning);
else if (lvl == "error")
SetLogLevel(LogLevel::Error);
} else
{
SetLogLevel(LogLevel::Info);
Log("AIOENET_LOG_LEVEL not set, defaulting to 'debInfoug'");
}
std::time_t start_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
Log(std::string("AIOeNET Daemon ") + BuildInfo::Version + " STARTING, git=" + BuildInfo::GitDescribe + ", built=" + BuildInfo::BuildUtc + ", it is now: " + std::string(std::ctime(&start_time)));
struct sigaction sigIntHandler;
sigIntHandler.sa_handler = abort_handler;
sigemptyset(&sigIntHandler.sa_mask);
sigIntHandler.sa_flags = 0;
sigaction(SIGINT, &sigIntHandler, NULL);
sigaction(SIGABRT, &sigIntHandler, NULL);
sigaction(SIGTERM, &sigIntHandler, NULL);
if (argc < 2)
{
Trace("Warning: no tcp port specified. Using default: " + std::to_string(ControlListenPort));
Trace("Usage: " + std::string(argv[0]) + " {port_to_listen — (i.e., 18767)}");
}
else
sscanf(argv[1], "%d", &ControlListenPort);
AdcListenPort = ControlListenPort + 1;
Trace("Control port: " + std::to_string(ControlListenPort));
Trace("ADC stream port: " + std::to_string(AdcListenPort));
}
void OpenDevFile()
{
constexpr const char *deviceDirectory = "/dev/apci";
apci = -1;
std::error_code ec;
const bool exists = std::filesystem::exists(deviceDirectory, ec);
if (ec)
{
SetDaqUnavailable(DaqStatus::OpenFailed, ec.value(), deviceDirectory,
"Cannot inspect /dev/apci: " + ec.message());
Warn("DAQ unavailable: cannot inspect /dev/apci: " + ec.message());
return;
}
if (!exists)
{
SetDaqUnavailable(DaqStatus::DeviceDirectoryMissing, ENOENT,
deviceDirectory, "/dev/apci is not present");
Warn("DAQ unavailable: /dev/apci is not present; continuing in diagnostics-only mode");
return;
}
std::filesystem::directory_iterator it(deviceDirectory, ec);
const std::filesystem::directory_iterator end;
if (ec)
{
SetDaqUnavailable(DaqStatus::OpenFailed, ec.value(), deviceDirectory,
"Cannot enumerate /dev/apci: " + ec.message());
Warn("DAQ unavailable: cannot enumerate /dev/apci: " + ec.message());
return;
}
if (it == end)
{
SetDaqUnavailable(DaqStatus::NoDeviceFound, ENODEV, deviceDirectory,
"/dev/apci contains no device file");
Warn("DAQ unavailable: /dev/apci contains no device file; continuing in diagnostics-only mode");
return;
}
const std::string deviceFile = it->path().string();
apci = open(deviceFile.c_str(), O_RDONLY | O_CLOEXEC);
if (apci < 0)
{
const int savedErrno = errno;
SetDaqUnavailable(DaqStatus::OpenFailed, savedErrno, deviceFile,
std::string("open failed: ") + std::strerror(savedErrno));
Warn("DAQ unavailable: open(" + deviceFile + ") failed: " +
std::strerror(savedErrno) + "; continuing in diagnostics-only mode");
return;
}
SetDaqReady(deviceFile);
Log("Opened DAQ device @ " + deviceFile);
}
void Bind(int &Socket, int &Port, void *structaddr, int iNET)
{
struct sockaddr_in *addr4 = static_cast<sockaddr_in *>(structaddr);
struct sockaddr_in6 *addr6 = static_cast<sockaddr_in6 *>(structaddr);
int result = -1;
Socket = socket(iNET, SOCK_STREAM, 0);
if (Socket < 0) {
Error(std::string("socket() failed: ") + std::strerror(errno));
exit(EXIT_FAILURE);
}
SetCloseOnExec(Socket, "listen socket");
int opt = 1;
if (setsockopt(Socket, SOL_SOCKET, SO_REUSEADDR,
&opt, sizeof(opt)) < 0) {
Error(std::string("setsockopt(SO_REUSEADDR) failed: ")
+ std::strerror(errno));
CloseFd(Socket, "listen socket after setsockopt failure");
exit(EXIT_FAILURE);
}
if (iNET == AF_INET) {
std::memset(addr4, 0, sizeof(*addr4));
addr4->sin_family = AF_INET;
addr4->sin_port = htons(static_cast<short>(Port));
addr4->sin_addr.s_addr = INADDR_ANY;
result = bind(Socket,
reinterpret_cast<struct sockaddr *>(addr4),
sizeof(sockaddr_in));
}
else {
std::memset(addr6, 0, sizeof(*addr6));
addr6->sin6_family = AF_INET6;
addr6->sin6_port = htons(static_cast<short>(Port));
addr6->sin6_addr = IN6ADDR_ANY_INIT;
addr6->sin6_scope_id = 0;
result = bind(Socket,
reinterpret_cast<struct sockaddr *>(addr6),
sizeof(sockaddr_in6));
}
if (result < 0) {
int savedErrno = errno;
Error("Bind on port " + std::to_string(Port)
+ " failed: " + std::strerror(savedErrno));
CloseFd(Socket, "listen socket after bind failure");
exit(EXIT_FAILURE);
}
}
#if 0 // pre-unbind
void Bind(int &Socket, int &Port, void *structaddr, int iNET)
{
struct sockaddr_in *addr4 = (sockaddr_in *)structaddr;
struct sockaddr_in6 *addr6 = (sockaddr_in6 *)structaddr;
int result = -1;
if ((Socket = socket(iNET, SOCK_STREAM, 0)) == 0)
{
perror("socket failed");
exit(EXIT_FAILURE);
}
int opt = 1;
if (setsockopt(Socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)) < 0)
{
Error("setsockopt failed");
perror("setsockopt failed");
exit(EXIT_FAILURE);
}
int yes = 1;
setsockopt(Socket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
if (iNET == AF_INET)
{
addr4->sin_family = AF_INET;
addr4->sin_port = htons(static_cast<short>(Port));
addr4->sin_addr.s_addr = INADDR_ANY;
result = bind(Socket, (struct sockaddr *)addr4, sizeof(sockaddr_in));
}
else // if (iNET == AF_INET6)
{
memset(addr6, 0, sizeof(*addr6));
addr6->sin6_family = AF_INET6;
addr6->sin6_port = htons(static_cast<short>(Port));
addr6->sin6_flowinfo = 0; // 0x607ACCE5;
addr6->sin6_addr = IN6ADDR_ANY_INIT;
addr6->sin6_scope_id = 0;
// addr6->sin6_scope_id = 0x0e;
result = bind(Socket, (struct sockaddr *)addr6, sizeof(sockaddr_in6));
}
if (result < 0)
{
Error("Bind on port " + std::to_string(Port) + " failed");
exit(EXIT_FAILURE);
}
// if (Port == ControlListenPort)
// controlSocket = Socket;
// if (Port == AdcListenPort)
// adcSocket = Socket;
}
#endif
void Listen(int &Socket, int num)
{
if (listen(Socket, num) < 0) // 32 connections; soft-cap or hard-cap?
{
Error("listen(ControlSocket) failed");
perror("listen(ControlSocket) failed");
exit(EXIT_FAILURE);
}
}
void *ControlListenerThread(void *arg)
{
const int iNET = static_cast<int>(reinterpret_cast<std::intptr_t>(arg));
sockaddr_storage controlAddress{};
int controlSocket = -1;
const socklen_t controlAddressSize = sizeof(controlAddress);
Bind(controlSocket, ControlListenPort, &controlAddress, iNET == AF_INET6 ? AF_INET6 : AF_INET);
Trace("Listen for Control Socket");
Listen(controlSocket, 32);
HandleNewControlClients(controlSocket, controlAddressSize, controlAddress);
CloseFd(controlSocket, "control listen socket");
return nullptr;
}
struct TAdcConnectionMonitorContext
{
int Socket = -1;
TAdcConnectionId ConnectionId = ADC_INVALID_CONNECTION_ID;
};
static bool SocketWouldBlock(int error)
{
if (error == EAGAIN)
return true;
#if EWOULDBLOCK != EAGAIN
if (error == EWOULDBLOCK)
return true;
#endif
return false;
}
static bool SendAdcHello(int socket, TAdcConnectionId connectionId)
{
const __u32 helloAdc = static_cast<__u32>(connectionId) | 0x80000000u;
const ssize_t bytesSent = SendAll(socket, &helloAdc, sizeof(helloAdc));
if (bytesSent != static_cast<ssize_t>(sizeof(helloAdc)))
{
Error("TCP send of ADC Hello failed (" + std::to_string(bytesSent) + " != " + std::to_string(sizeof(helloAdc)) + ")");
return false;
}
Log("Sent ADC Hello: socket=" + std::to_string(socket) + ", connection=" + std::to_string(connectionId) + ", wire=" + to_hex<__u32>(helloAdc));
return true;
}
static void *AdcConnectionMonitorThread(void *arg)
{
std::unique_ptr<TAdcConnectionMonitorContext> context(static_cast<TAdcConnectionMonitorContext *>(arg));
int socket = context->Socket;
const TAdcConnectionId connectionId = context->ConnectionId;
pollfd descriptor{};
descriptor.fd = socket;
descriptor.events = POLLIN;
#ifdef POLLRDHUP
descriptor.events |= POLLRDHUP;
#endif
while (!done)
{
descriptor.revents = 0;
const int status = poll(&descriptor, 1, 1000);
if (status < 0)
{
if (errno == EINTR)
continue;
Error("poll() failed for ADC data connection " + std::to_string(connectionId) + ": " + std::strerror(errno));
break;
}
if (status == 0)
continue;
short disconnectEvents = POLLERR | POLLHUP | POLLNVAL;
#ifdef POLLRDHUP
disconnectEvents |= POLLRDHUP;
#endif
if ((descriptor.revents & disconnectEvents) != 0)
break;
if ((descriptor.revents & POLLIN) != 0)
{
char byte = 0;
const ssize_t received = recv(socket, &byte, sizeof(byte), MSG_PEEK | MSG_DONTWAIT);
if (received == 0)
break;
if (received > 0)
{
Warn("Unexpected client data received on ADC streaming connection " + std::to_string(connectionId) + "; closing the connection");
break;
}
if (!SocketWouldBlock(errno) && errno != EINTR)
break;
}
}
if (socket >= 0)
shutdown(socket, SHUT_RDWR);
AdcDataConnectionClosed(connectionId, socket);
CloseFd(socket, "ADC data connection");
Log("ADC data connection closed: connection=" + std::to_string(connectionId));
return nullptr;
}
void HandleNewAdcClients(int listenSocket)
{
Trace("Accept for ADC streaming");
while (!done)
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(listenSocket, &readfds);
timeval timeout{};
timeout.tv_sec = 1;
const int status = select(listenSocket + 1, &readfds, nullptr, nullptr, &timeout);
if (status < 0)
{
if (errno == EINTR)
continue;
Error("select() failed for ADC listener: " + std::string(std::strerror(errno)));
break;
}
if (status == 0)
continue;
sockaddr_storage peerAddress{};
socklen_t peerAddressSize = sizeof(peerAddress);
int newSocket = accept(listenSocket, reinterpret_cast<sockaddr *>(&peerAddress), &peerAddressSize);
if (newSocket < 0)
{
if (errno == EINTR)
continue;
Error("accept() failed for ADC connection: " + std::string(std::strerror(errno)));
continue;
}
SetCloseOnExec(newSocket, "ADC data socket");
timeval sendTimeout{};
sendTimeout.tv_sec = 1;
if (setsockopt(newSocket, SOL_SOCKET, SO_SNDTIMEO, &sendTimeout, sizeof(sendTimeout)) < 0)
Warn("setsockopt(SO_SNDTIMEO) failed for ADC data socket " + std::to_string(newSocket) + ": " + std::strerror(errno));
const TAdcConnectionId connectionId = AdcRegisterDataConnection(newSocket);
if (connectionId == ADC_INVALID_CONNECTION_ID)
{
Error("Unable to allocate an ADC data connection ID");
CloseFd(newSocket, "unregistered ADC data socket");
continue;
}
if (!SendAdcHello(newSocket, connectionId))
{
AdcDataConnectionClosed(connectionId, newSocket);
CloseFd(newSocket, "ADC data socket after Hello failure");
continue;