├── .gitmodules ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── DEVELOPING.md ├── LICENSE ├── Makefile ├── README.md ├── build.zig ├── config.toml ├── dev └── shell.nix ├── doc ├── 2020-08_RFC_01_adding_process_monitoring.md ├── Using-Profiler-checking-performance.md └── tracy_sc.png ├── docker ├── Dockerfile ├── README.md ├── config.toml └── grabzigbinary.sh ├── flake.lock ├── flake.nix ├── iotmonitor.zig ├── leveldb.zig ├── man ├── iotmonitor.1.md ├── sections_to_cover.txt └── viewdoc.sh ├── mqttlib.zig ├── nozig-tracy └── src │ └── lib.zig ├── processlib.zig ├── snapcraft ├── README.md └── snap │ └── snapcraft.yaml ├── topics.zig └── version.zig /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "paho.mqtt.c"] 2 | path = paho.mqtt.c 3 | url = https://github.com/eclipse/paho.mqtt.c 4 | [submodule "routez"] 5 | path = routez 6 | url = https://github.com/frett27/routez 7 | [submodule "zig-tracy"] 8 | path = zig-tracy 9 | url = https://github.com/nektro/zig-tracy 10 | [submodule "tracy"] 11 | path = tracy 12 | url = https://github.com/wolfpld/tracy 13 | [submodule "zig-clap"] 14 | path = zig-clap 15 | url = https://github.com/Hejsil/zig-clap 16 | [submodule "zig-toml"] 17 | path = zig-toml 18 | url = https://github.com/aeronavery/zig-toml 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Version 0.2.5 4 | 5 | - add nix support for distribution and compilation 6 | - add http access to iot monitoring, see configuration options 7 | - move to zig 0.9.0 8 | 9 | 10 | ## Version 0.2.2 11 | 12 | #### 2021-06-12 13 | - add clientid in config to permit multiple instances of iotmonitor 14 | - fix 0.8 compilation 15 | 16 | #### 2020-11-22 17 | - move to zig release 0.7 18 | 19 | #### 2020-10-16 20 | 21 | - add mqtt reconnect logic, as mqtt sync client does not have the "reconnect" builtin capabilities yet. 22 | 23 | ## Version 0.2 24 | 25 | #### 2020-09-13 26 | 27 | - add process monitoring 28 | - fix docker to use the zig master branch to compile 29 | 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at frett27@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Developing on the project 4 | 5 | when-changed is a simple python command ligne that auto recompile the project, when files changes 6 | 7 | when-changed *.zig zig build 8 | when-changed leveldb.zig ../zig/zig test leveldb.zig -l leveldb -l c -l c++ 9 | 10 | 11 | 12 | ### Possible Improvments 13 | 14 | - Use a full parser 15 | 16 | seems a target port for zig in antlr4, is a good idea 17 | https://github.com/antlr/antlr4/blob/master/doc/creating-a-language-target.md 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | BINDIR=/usr/bin/iotmonitor 3 | MANDIR=/usr/share/man/man1 4 | 5 | iotmonitor: 6 | cd paho.mqtt.c && cmake -DPAHO_BUILD_STATIC=true 7 | cd paho.mqtt.c && make 8 | mkdir -p bin 9 | zig build 10 | pandoc man/iotmonitor.1.md -s -t man | gzip -c > bin/iotmonitor.1.gz 11 | 12 | install: 13 | cp bin/iotmonitor $(BINDIR) 14 | cp bin/iotmonitor.1.gz $(MANDIR)/ 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## IOTMonitor project 3 | 4 | IotMonitor is an effortless and lightweight mqtt monitoring for devices (things) and agents on Linux. 5 | 6 | IotMonitor aims to solve the "always up" problem of large IOT devices and agents system. This project is successfully used every day for running smart home automation system. 7 | Considering large and longlived running mqtt systems can hardly rely only on monolytics plateforms, the reality is always composite as some agents or functionalities increase with time. Diversity also occurs in running several programming languages implementation for agents. 8 | 9 | This project offers a simple command line, as in *nix system, to monitor MQTT device or agents system. MQTT based communication devices (IOT) and agents are watched, and alerts are emitted if devices or agents are not responding any more. Declared software agents are restarted by iotmonitor when crashed. 10 | 11 | In the behaviour, once the mqtt topics associated to a thing or agent is declared, IotMonitor records and restore given MQTT "states topics" as they go and recover. It helps reinstalling IOT things state, to avoid lots of administration tasks. 12 | 13 | IotMonitor use a TOML config file. Each device has an independent configured communication message time out. When the device stop communication on this topic, the iotmonitor publish a specific monitoring failure topic for the lots device, with the latest contact timestamp. This topic is labelled : 14 | 15 | home/monitoring/expire/[device_name] 16 | 17 | This topic can then be displayed or alerted to inform that the device or agent is not working properly. 18 | 19 | This project is based on C Paho MQTT client library, use leveldb as state database. 20 | 21 | 22 | 23 | ### Running the project on linux 24 | 25 | #### Using Nix 26 | 27 | Install Nix, [https://nixos.org/download.html](https://nixos.org/download.html) 28 | 29 | 30 | 31 | then, using the following `shell.nix` file, 32 | 33 | ``` 34 | { nixpkgs ? , iotstuff ? import (fetchTarball 35 | "https://github.com/mqttiotstuff/nix-iotstuff-repo/archive/9b12720.tar.gz") 36 | { } }: 37 | 38 | iotstuff.pkgs.mkShell rec { buildInputs = [ iotstuff.iotmonitor ]; } 39 | ``` 40 | 41 | run : 42 | 43 | ``` 44 | nix-shell shell.nix 45 | ``` 46 | 47 | or : (with the shell.nix in the folder) 48 | 49 | ``` 50 | nix-shell 51 | ``` 52 | 53 | #### Using Nix Flake 54 | 55 | 56 | nix run git+https://github.com/mqttiotstuff/iotmonitor?submodules=1 57 | 58 | 59 | build with flake : 60 | 61 | git clone --recursive https://github.com/mqttiotstuff/iotmonitor 62 | nix build "git+file://$(pwd)?submodules=1" 63 | 64 | 65 | #### Using docker, 66 | 67 | [see README in docker subfolder, for details and construct the image](docker/README.md) 68 | 69 | 70 | launch the container from image : 71 | 72 | ```bash 73 | docker run --rm -d -u $(id --user) -v `pwd`:/config iotmonitor 74 | ``` 75 | 76 | 77 | 78 | #### From scratch 79 | 80 | for building the project, the following elements are needed : 81 | 82 | - leveldb library (used for storing stated) 83 | - C compiler (builds essentials) 84 | - cmake 85 | - zig : 0.9.1 86 | 87 | then launch the following commands : 88 | 89 | ```bash 90 | git clone --recursive https://github.com/frett27/iotmonitor 91 | cd iotmonitor 92 | cd paho.mqtt.c 93 | cmake -DPAHO_BUILD_STATIC=true . 94 | make 95 | cd .. 96 | 97 | mkdir bin 98 | zig build -Dcpu=baseline -Dtarget=native-native-gnu 99 | ``` 100 | 101 | 102 | 103 | 104 | ### Configuration File 105 | 106 | The configuration is defined in the `config.toml` TOML file, (see an example in the root directory) 107 | 108 | A global Mqtt broker configuration section is defined using a heading `[mqtt]` 109 | the following parameters are found : 110 | 111 | ```toml 112 | [mqtt] 113 | serverAddress="tcp://localhost:1883" 114 | baseTopic="home/monitoring" 115 | user="" 116 | password="" 117 | ``` 118 | 119 | An optional clientid can also be specified to change the mqtt clientid, a default "iotmonitor" is provided 120 | 121 | 122 | #### Device declaration 123 | 124 | In the configuration toml file, each device is declared in a section using a "device_" prefix 125 | in the section : the following elements can be found : 126 | 127 | ```toml 128 | [device_esp04] 129 | watchTimeOut=60 130 | helloTopic="home/esp04" 131 | watchTopics="home/esp04/sensors/#" 132 | stateTopics="home/esp04/actuators/#" 133 | ``` 134 | 135 | - `watchTimeOut` : watch dog for alive state, when the timeout is reached without and interactions on watchTopics, then iotmonitor trigger an expire message for the device 136 | - `helloTopic` : the topic to observe to welcome the device. This topic trigger the state recovering for the device and agents. IotMonitor, resend the previous stored `stateTopics`, the content of the helloTopic is not used (every first mqtt topic can be used to restore state) 137 | - `watchTopics` : the topic pattern to observe to know the device is alive 138 | - `stateTopics` : list of topics for recording the states and reset them as they are welcomed 139 | 140 | #### Agents declarations 141 | 142 | Agents are declared using an "agent_" prefix in the section. Agents are devices with an associated command line (`exec` config key) that trigger the start of the software agent. IotMonitor checks periodically if the process is running, and relaunch it if needed. 143 | 144 | ```toml 145 | [agent_ledboxdaemon] 146 | exec="source ~/mqttagents/p3/bin/activate;cd ~/mqttagents/mqtt-agent-ledbox;python3 ledboxdaemon.py" 147 | watchTopics="home/agents/ledbox/#" 148 | ``` 149 | 150 | IotMonitor running the processes identify the process using a specific bash command line containing an IOTMONITOR tag, which is recognized to detect if the process is running. Monitored processes are detached from the iotmonitor process, avoiding to relaunch the whole system in the case of restarting the `iotmonitor` process. 151 | 152 | Agents may also have `helloTopic`, `stateTopics` and `watchTimeOut` as previously described. 153 | 154 | #### State restoration for things and agents 155 | 156 | At startup OR when the `helloTopic` is fired, iotmonitor fire the previousely recorded states on mqtt, this permit the device (things), to take it's previoulsy state, as if it has not been stopped.. All mqtt recorded states (`stateTopics`) are backuped by iotmonitor in a leveldb database. 157 | For practical reasons, this permit to centralize the state, and restore them when an iot device has rebooted. If used this functionnality, reduce the need to implement a cold state storage for each agent or device. Starting or stopping iotmonitor, redefine the state for all elements. 158 | 159 | #### Monitoring iotmonitor :-) 160 | 161 | IotMonitor publish a counter on the `home/monitoring/up` topic every seconds. One can then monitor the iotmonitor externally. 162 | The counter is resetted at each startup. 163 | 164 | 165 | 166 | 167 | 168 | ### Credits 169 | 170 | - zig-toml : for zig toml parser 171 | - paho eclipse mqtt c library 172 | - levedb database 173 | - routez : for http server integration 174 | 175 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const fs = std.fs; 4 | const Builder = std.build.Builder; 5 | const ArrayList = std.ArrayList; 6 | const builtin = @import("builtin"); 7 | const Target = std.Target; 8 | const FileSource = std.build.FileSource; 9 | 10 | pub fn build(b: *Builder) void { 11 | const exe = b.addExecutable("iotmonitor", "iotmonitor.zig"); 12 | 13 | const target = b.standardTargetOptions(.{}); 14 | exe.setTarget(target); 15 | exe.setBuildMode(b.standardReleaseOptions()); 16 | 17 | exe.addPackagePath("toml", "zig-toml/src/toml.zig"); 18 | exe.addPackagePath("clap", "zig-clap/clap.zig"); 19 | exe.addPackage(.{ 20 | .name = "routez", 21 | .path = FileSource.relative("routez/src/routez.zig"), 22 | .dependencies = &[_]std.build.Pkg{.{ 23 | .name = "zuri", 24 | .path = FileSource.relative("routez/zuri/src/zuri.zig"), 25 | }}, 26 | }); 27 | 28 | const Activate_Tracy = false; 29 | 30 | if (Activate_Tracy) { 31 | exe.addPackage(.{ .name = "tracy", .path = FileSource.relative("zig-tracy/src/lib.zig") }); 32 | exe.addIncludeDir("tracy/"); 33 | exe.addLibPath("tracy/library/unix"); 34 | exe.linkSystemLibrary("tracy-debug"); 35 | } else { 36 | exe.addPackage(.{ .name = "tracy", .path = FileSource.relative("nozig-tracy/src/lib.zig") }); 37 | } 38 | 39 | exe.setOutputDir("bin"); 40 | // exe.setTarget(.{ .cpu = builtin.Arch.arm }); 41 | 42 | // stripping symbols reduce the size of the exe 43 | // exe.strip = true; 44 | exe.linkLibC(); 45 | 46 | // static add the paho mqtt library 47 | exe.addIncludeDir("paho.mqtt.c/src"); 48 | exe.addLibPath("paho.mqtt.c/build/output"); 49 | exe.addLibPath("paho.mqtt.c/src"); 50 | exe.addObjectFile("paho.mqtt.c/src/libpaho-mqtt3c.a"); 51 | 52 | // these libs are needed by leveldb backend 53 | exe.linkSystemLibrary("leveldb"); 54 | 55 | b.default_step.dependOn(&exe.step); 56 | } 57 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | 2 | [mqtt] 3 | serverAddress="tcp://localhost:1883" 4 | baseTopic="mybaseTopic/monitoring" 5 | user="" 6 | password="" 7 | 8 | [http] 9 | 10 | [device_nodered] 11 | watchTimeOut=2 12 | watchTopics="home/agents/nodered/watchdog" 13 | 14 | [agent_switchcontrol] 15 | exec="sleep 100" 16 | watchTopics="home/agents" 17 | # watchTimeOut= 18 | 19 | -------------------------------------------------------------------------------- /dev/shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | nixpkgs = builtins.fetchTarball { 3 | # nixpkgs-unstable (2021-10-28) 4 | url = "https://github.com/NixOS/nixpkgs/archive/d9c13cf44ec1b6de95cb1ba83c296611d19a71ae.tar.gz"; 5 | # sha256 = "1rqp9nf45m03mfh4x972whw2gsaz5x44l3dy6p639ib565g24rmh"; 6 | }; 7 | in 8 | { pkgs ? import nixpkgs { } }: 9 | 10 | pkgs.mkShell { 11 | nativeBuildInputs = with pkgs; [ 12 | cmake 13 | gdb 14 | ninja 15 | qemu 16 | zig 17 | leveldb 18 | libsodium 19 | ] ++ (with llvmPackages_13; [ 20 | clang 21 | clang-unwrapped 22 | lld 23 | llvm 24 | ]); 25 | 26 | hardeningDisable = [ "all" ]; 27 | } 28 | -------------------------------------------------------------------------------- /doc/2020-08_RFC_01_adding_process_monitoring.md: -------------------------------------------------------------------------------- 1 | # RFC adding process monitoring 2 | 3 | 4 | 5 | August 2020 6 | 7 | ### Summary 8 | 9 | IOTMonitor currently monitor MQTT devices or agents, by watching mqtt topics and associated states. 10 | 11 | in case of non response, iotmonitor send a MQTT alert in a specific MQTT topic linked to monitoring. 12 | 13 | 14 | 15 | ### Process monitoring goal 16 | 17 | This evolution it to also manage the external agents process management. Knowing if a process is healthy imply to know the functional behaviour and this is a hard trick to define externally (failure detection problem). 18 | 19 | Nonetheless, if the process periodically published its health, or having and external process than monitor it's health. In Using periodic MQTT healthchecks, this is then possible to handle the (kill/exec) command on the process to make the system work. 20 | 21 | This ability also simplify the managing of such system, this mean, when a process managing devices and providing MQTT watchdog health check, then an entry in IOTMonitor can be added to manage the process. 22 | 23 | 24 | 25 | ### Implementation Ideas 26 | 27 | As IOTMonitor launch the process, it can setup a unique identifier that can be used to track the process. process can be wrapped into bash exec , (using the command lines) with additional parameters, and especially the IOTMonitor id, to track it down. 28 | 29 | If the process does not respond periodically to mqtt topic, then the iot monitor can kill and restart the process, setting up a specific stdout log file and stderr log linked to the process. Process is detached from IOTMonitor session to be able to stop the IOTMonitor without to kill all process and stop all system's functionnalities. 30 | 31 | In this manner, there are no complex IPC to setup or invasive 32 | 33 | ### Additional concerns 34 | 35 | MQTT authentication on the behave of the process : this can be added in defining environment variable in the process launch. The command line can then take them to do the necessary transmission. 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /doc/Using-Profiler-checking-performance.md: -------------------------------------------------------------------------------- 1 | 2 | # Using Tracy to check performances 3 | 4 | ## Ubuntu 20, 5 | 6 | ### Install tracy library dependencies 7 | 8 | apt install libglfw3-dev libgtk-3-dev libcapstone-dev libtbb-dev 9 | 10 | cd tracy 11 | 12 | make -j -C profiler/build/unix debug release 13 | 14 | make -j -C library/unix debug release 15 | 16 | 17 | To launch iotmonitor with tracing, 18 | 19 | be sure the library path contains the tracy library 20 | 21 | export LD_LIBRARY_PATH=tracy/library/unix 22 | 23 | launch iotmonitor, and Tracy, 24 | 25 | ![](tracy_sc.png) 26 | 27 | -------------------------------------------------------------------------------- /doc/tracy_sc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqttiotstuff/iotmonitor/20d8da34b7e427ed6bc78cadd6e7b553c4e90b35/doc/tracy_sc.png -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 AS builder 2 | ARG COMMIT 3 | ARG VERSION 4 | 5 | RUN apt-get update 6 | RUN apt-get install -y build-essential git libleveldb-dev 7 | RUN apt-get install -y wget xz-utils 8 | RUN DEBIAN_FRONTEND="noninteractive" apt-get install -y cmake 9 | 10 | WORKDIR /build 11 | 12 | COPY grabzigbinary.sh . 13 | RUN chmod a+x grabzigbinary.sh 14 | RUN ./grabzigbinary.sh $VERSION $COMMIT 15 | 16 | RUN git clone --recursive https://github.com/mqttiotstuff/iotmonitor 17 | WORKDIR /build/iotmonitor 18 | RUN git checkout develop 19 | WORKDIR /build/iotmonitor/paho.mqtt.c 20 | RUN cmake -DPAHO_BUILD_STATIC=true . 21 | RUN make 22 | WORKDIR /build/iotmonitor 23 | RUN mkdir bin 24 | 25 | RUN ../zigbundle/zig build 26 | 27 | FROM ubuntu:20.04 28 | 29 | RUN apt-get update 30 | RUN apt-get install -y libleveldb-dev 31 | WORKDIR /iotmonitor/ 32 | COPY --from=builder /build/iotmonitor/bin/iotmonitor . 33 | RUN chmod a+rx iotmonitor 34 | 35 | WORKDIR /config 36 | 37 | CMD ["../iotmonitor/iotmonitor"] 38 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Docker for iotmonitor 2 | 3 | this docker create the iotmonitor image, for x64 platefoms 4 | note that if you run the iotmonitor as a container, processes must be also hosted in the container. 5 | 6 | # Building the image using the zig master 7 | 8 | The VERSION variable mention the master version 9 | Optional, The COMMIT variable may be specified for the ZIG commit to use (zig dev repository), if not set, official RELEASE zig version IS used 10 | (check http://ziglang.org/downloads for the current value) 11 | 12 | for officiel release zig version: 13 | 14 | docker build --build-arg VERSION=0.9.1 -t iotmonitor . 15 | 16 | docker build --build-arg COMMIT=45212e3b3 --build-arg VERSION=0.9.0-dev.103 -t iotmonitor . 17 | 18 | # Running the container 19 | 20 | The current folder must have the config.toml configuration file, and write access to the executing USER, to create the leveldb database associated to device states 21 | 22 | docker run --rm -d -u $(id --user) -v `pwd`:/config iotmonitor 23 | 24 | 25 | # RPI compilation (to be tested) 26 | 27 | 28 | docker run -it -v $HOME/.dockerpi:/sdcard lukechilds/dockerpi 29 | 30 | 31 | -------------------------------------------------------------------------------- /docker/config.toml: -------------------------------------------------------------------------------- 1 | 2 | [mqtt] 3 | user="USER" 4 | password="PASSWORD" 5 | serverAddress="tcp://192.168.4.16:1883" 6 | 7 | 8 | [device_nodered] 9 | watchTimeOut=2 10 | watchTopics="home/agents/nodered/watchdog" 11 | 12 | 13 | [device_agent_wifi_measure] 14 | watchTimeOut=30 15 | watchTopics="home/esp10/sensors/#" 16 | 17 | [device_esp10] 18 | watchTimeOut=60 19 | helloTopic="home/esp10" 20 | watchTopics="home/esp10/sensors/#" 21 | stateTopics="home/esp10/actuators/#" 22 | 23 | [device_esp08] 24 | watchTimeOut=60 25 | helloTopic="home/esp08" 26 | watchTopics="home/esp08/sensors/#" 27 | stateTopics="home/esp08/actuators/relay1" 28 | 29 | [device_esp04] 30 | watchTimeOut=60 31 | helloTopic="home/esp04" 32 | watchTopics="home/esp04/sensors/#" 33 | stateTopics="home/esp04/actuators/#" 34 | 35 | [device_esp03] 36 | watchTimeOut=60 37 | helloTopic="home/esp03" 38 | watchTopics="home/esp03/sensors/#" 39 | stateTopics="home/esp03/actuators/#" 40 | 41 | # METEO 42 | [device_esp05] 43 | watchTimeOut=1200 44 | helloTopic="home/esp05" 45 | watchTopics="home/esp03/sensors/#" 46 | 47 | -------------------------------------------------------------------------------- /docker/grabzigbinary.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=$1 4 | COMMIT=$2 5 | 6 | if [ -z "${COMMIT}"] 7 | then 8 | echo "Using official build for zig" 9 | BASEFILENAME=zig-linux-x86_64-$VERSION 10 | DOWNLOADBASE=https://ziglang.org/download/$VERSION 11 | else 12 | echo "Using nightly build releases for zig" 13 | BASEFILENAME=zig-linux-x86_64-$VERSION+$COMMIT 14 | DOWNLOADBASE=https://ziglang.org/builds 15 | fi 16 | 17 | wget $DOWNLOADBASE/$BASEFILENAME.tar.xz 18 | xz -d $BASEFILENAME.tar.xz 19 | tar xvf $BASEFILENAME.tar 20 | 21 | mv $BASEFILENAME zigbundle 22 | 23 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1642700792, 6 | "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "narHash": "sha256-M6bJShji9AIDZ7Kh7CPwPBPb/T7RiVev2PAcOi4fxDQ=", 21 | "type": "tarball", 22 | "url": "https://github.com/NixOS/nixpkgs/archive/22.05.tar.gz" 23 | }, 24 | "original": { 25 | "type": "tarball", 26 | "url": "https://github.com/NixOS/nixpkgs/archive/22.05.tar.gz" 27 | } 28 | }, 29 | "root": { 30 | "inputs": { 31 | "flake-utils": "flake-utils", 32 | "nixpkgs": "nixpkgs" 33 | } 34 | } 35 | }, 36 | "root": "root", 37 | "version": 7 38 | } 39 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Flake for building Iotmonitor"; 3 | # inputs = [ zig git cmake leveldb pandoc ]; 4 | inputs = { 5 | nixpkgs.url = "https://github.com/NixOS/nixpkgs/archive/22.05.tar.gz"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils }: 10 | 11 | flake-utils.lib.eachDefaultSystem (system: 12 | let pkgs = nixpkgs.legacyPackages.${system}; 13 | in { 14 | packages.iotmonitor = 15 | # Notice the reference to nixpkgs here. 16 | pkgs.stdenv.mkDerivation { 17 | name = "iotmonitor"; 18 | src = self; 19 | buildInputs = [ pkgs.zig pkgs.git pkgs.cmake pkgs.leveldb pkgs.pandoc ]; 20 | configurePhase = '' 21 | ls 22 | zig version 23 | ''; 24 | 25 | buildPhase = '' 26 | make 27 | zig build 28 | ''; 29 | 30 | installPhase = '' 31 | mkdir -p $out/bin 32 | mv bin/iotmonitor $out/bin 33 | ''; 34 | 35 | }; 36 | 37 | defaultPackage = self.packages.${system}.iotmonitor; 38 | } 39 | ); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /iotmonitor.zig: -------------------------------------------------------------------------------- 1 | // 2 | // IOT Monitor - monitor and recover state for iot device and software agents 3 | // 4 | // pfreydiere - 2019 - 2022 5 | // 6 | 7 | const std = @import("std"); 8 | const json = std.json; 9 | const debug = std.debug; 10 | const log = std.log; 11 | const assert = debug.assert; 12 | const mem = std.mem; 13 | const os = std.os; 14 | const io = std.io; 15 | const version = @import("version.zig"); 16 | 17 | // used for sleep, and other, it may be removed 18 | // to relax libC needs 19 | const c = @cImport({ 20 | @cInclude("stdio.h"); 21 | @cInclude("unistd.h"); 22 | @cInclude("signal.h"); 23 | @cInclude("time.h"); 24 | @cInclude("string.h"); 25 | }); 26 | 27 | const leveldb = @import("leveldb.zig"); 28 | const mqtt = @import("mqttlib.zig"); 29 | const processlib = @import("processlib.zig"); 30 | const topics = @import("topics.zig"); 31 | const toml = @import("toml"); 32 | const clap = @import("clap"); 33 | 34 | // profiling, to check performance on functions 35 | // this is mocked in the build.zig (activate this tracy library) 36 | const tracy = @import("tracy"); 37 | 38 | const stdoutFile = std.io.getStdOut(); 39 | const out = std.fs.File.writer(stdoutFile); 40 | 41 | const Verbose = false; 42 | 43 | // This structure defines the process informations 44 | // with live agent running, this permit to track the process and 45 | // relaunch it if needed 46 | // 47 | const AdditionalProcessInformation = struct { 48 | // pid is to track the process while running 49 | pid: ?i32 = undefined, 50 | // process identifier attributed by IOTMonitor, to track existing processes 51 | // processIdentifier: []const u8 = "", 52 | exec: []const u8 = "", 53 | 54 | // last time the process is restarted 55 | lastRestarted: c.time_t = 0, 56 | 57 | // number of time, the process is restarted 58 | restartedCount: u64 = 0, 59 | }; 60 | 61 | const MonitoringInfo = struct { 62 | // name of the device 63 | name: []const u8 = "", 64 | watchTopics: []const u8, 65 | nextContact: c.time_t, 66 | timeoutValue: u32 = 30, 67 | stateTopics: ?[]const u8 = null, 68 | helloTopic: ?[]const u8 = null, 69 | helloTopicCount: u64 = 0, 70 | allocator: mem.Allocator, 71 | 72 | // in case of process informations, 73 | // used to relaunch or not the process, permitting to 74 | // take a process out of the monitoring, and then reintegrate it 75 | enabled: bool = true, 76 | 77 | associatedProcessInformation: ?*AdditionalProcessInformation = null, 78 | 79 | fn init(allocator: mem.Allocator) !*MonitoringInfo { 80 | const device = try allocator.create(MonitoringInfo); 81 | device.allocator = allocator; 82 | device.stateTopics = null; 83 | device.helloTopic = null; 84 | device.helloTopicCount = 0; 85 | device.timeoutValue = 30; 86 | device.associatedProcessInformation = null; 87 | device.enabled = true; 88 | 89 | return device; 90 | } 91 | fn deinit(self: *MonitoringInfo) void { 92 | self.allocator.destroy(self); 93 | } 94 | 95 | fn updateNextContact(device: *MonitoringInfo) !void { 96 | _ = c.time(&device.*.nextContact); 97 | device.*.nextContact = device.*.nextContact + @intCast(c_long, device.*.timeoutValue); 98 | } 99 | 100 | fn hasExpired(device: *MonitoringInfo) !bool { 101 | var currentTime: c.time_t = undefined; 102 | _ = c.time(¤tTime); 103 | const diff = c.difftime(currentTime, device.*.nextContact); 104 | if (diff > 0) return true; 105 | return false; 106 | } 107 | }; 108 | 109 | fn stripLastWildCard(watchValue: []const u8) ![]const u8 { 110 | assert(watchValue.len > 0); 111 | if (watchValue[watchValue.len - 1] == '#') { 112 | return watchValue[0 .. watchValue.len - 2]; 113 | } 114 | return watchValue; 115 | } 116 | 117 | test "test update time" { 118 | var d = MonitoringInfo{ 119 | .timeoutValue = 1, 120 | .watchTopics = "", 121 | .nextContact = undefined, 122 | .allocator = undefined, 123 | .helloTopic = undefined, 124 | .stateTopics = undefined, 125 | .associatedProcessInformation = undefined, 126 | }; 127 | try d.updateNextContact(); 128 | _ = c.sleep(3); 129 | debug.assert(try d.hasExpired()); 130 | 131 | d.timeoutValue = 20; 132 | try d.updateNextContact(); 133 | 134 | _ = c.sleep(3); 135 | debug.assert(!try d.hasExpired()); 136 | } 137 | 138 | pub fn secureZero(s: []u8) void { 139 | var i: u32 = 0; 140 | while (i < s.len) { 141 | s[i] = '\x00'; 142 | i = i + 1; 143 | } 144 | } 145 | 146 | // parse the device info, 147 | // device must have a watch topics 148 | fn parseDevice(allocator: mem.Allocator, name: []const u8, entry: *toml.Table) !*MonitoringInfo { 149 | const device = try MonitoringInfo.init(allocator); 150 | errdefer device.deinit(); 151 | const allocName = try allocator.alloc(u8, name.len + 1); 152 | 153 | secureZero(allocName); 154 | 155 | std.mem.copy(u8, allocName, name); 156 | device.name = allocName; 157 | 158 | if (entry.keys.get("exec")) |exec| { 159 | const execValue = exec.String; 160 | assert(execValue.len > 0); 161 | const execCommand = try allocator.allocSentinel(u8, execValue.len, 0); 162 | mem.copy(u8, execCommand, execValue); 163 | const additionalStructure = try allocator.create(AdditionalProcessInformation); 164 | additionalStructure.exec = execCommand; 165 | additionalStructure.pid = null; 166 | additionalStructure.lastRestarted = 0; 167 | additionalStructure.restartedCount = 0; 168 | device.associatedProcessInformation = additionalStructure; 169 | } 170 | 171 | if (entry.keys.get("watchTopics")) |watch| { 172 | // there may have a wildcard at the end 173 | // strip it to compare to the received topic 174 | const watchValue = watch.String; 175 | assert(watchValue.len > 0); 176 | const strippedLastWildCard = try stripLastWildCard(watchValue); 177 | const stopic = try allocator.alloc(u8, strippedLastWildCard.len); 178 | mem.copy(u8, stopic, strippedLastWildCard); 179 | device.watchTopics = stopic; 180 | if (Verbose) { 181 | _ = try out.print("add {s} to device {s} \n", .{ device.name, device.watchTopics }); 182 | } 183 | } else { 184 | return error.DEVICE_MUST_HAVE_A_WATCH_TOPIC; 185 | } 186 | 187 | if (entry.keys.get("stateTopics")) |statewatch| { 188 | // there may have a wildcard at the end 189 | // strip it to compare to the received topic 190 | const watchValue = statewatch.String; 191 | assert(watchValue.len > 0); 192 | const strippedLastWildCard = try stripLastWildCard(watchValue); 193 | const stopic = try allocator.alloc(u8, strippedLastWildCard.len); 194 | mem.copy(u8, stopic, strippedLastWildCard); 195 | device.stateTopics = stopic; 196 | if (Verbose) { 197 | _ = try out.print("add {s} to device {s} \n", .{ device.name, device.stateTopics }); 198 | } 199 | } 200 | 201 | if (entry.keys.get("helloTopic")) |hello| { 202 | const helloValue = hello.String; 203 | assert(helloValue.len > 0); 204 | const stopic = try allocator.alloc(u8, helloValue.len); 205 | mem.copy(u8, stopic, helloValue); 206 | device.helloTopic = stopic; 207 | if (Verbose) { 208 | _ = try out.print("hello topic for device {s}\n", .{device.helloTopic}); 209 | } 210 | } 211 | 212 | if (entry.keys.get("watchTimeOut")) |timeout| { 213 | const timeOutValue = timeout.Integer; 214 | device.timeoutValue = @intCast(u32, timeOutValue); 215 | if (Verbose) { 216 | _ = try out.print("watch timeout for topic for device {s}\n", .{device.helloTopic}); 217 | } 218 | } 219 | 220 | try device.updateNextContact(); 221 | return device; 222 | } 223 | 224 | const Config = struct { clientId: []u8, mqttBroker: []u8, user: []u8, password: []u8, clientid: []u8, mqttIotmonitorBaseTopic: []u8 }; 225 | const HttpServerConfig = struct { activateHttp: bool = true, listenAddress: []u8, port: u16 = 8079 }; 226 | 227 | var MqttConfig: *Config = undefined; 228 | var HttpConfig: *HttpServerConfig = undefined; 229 | 230 | fn parseTomlConfig(allocator: mem.Allocator, _alldevices: *AllDevices, filename: []const u8) !void { 231 | const t = tracy.trace(@src()); 232 | defer t.end(); 233 | 234 | // getting config parameters 235 | 236 | var parser: toml.Parser = undefined; 237 | defer parser.deinit(); 238 | 239 | var config = try toml.parseFile(allocator, filename, &parser); // no custom parser 240 | defer config.deinit(); 241 | 242 | var it = config.keys.iterator(); 243 | while (it.next()) |e| { 244 | // get table value 245 | switch (e.value_ptr.*) { 246 | toml.Value.Table => |table| { 247 | if (table.name.len >= 7) { 248 | const DEVICEPREFIX = "device_"; 249 | const AGENTPREFIX = "agent_"; 250 | const isDevice = mem.eql(u8, table.name.ptr[0..DEVICEPREFIX.len], DEVICEPREFIX); 251 | const isAgent = mem.eql(u8, table.name.ptr[0..AGENTPREFIX.len], AGENTPREFIX); 252 | if (isDevice or isAgent) { 253 | if (Verbose) { 254 | try out.print("device found :{s}\n", .{table.name}); 255 | } 256 | var prefixlen = AGENTPREFIX.len; 257 | if (isDevice) prefixlen = DEVICEPREFIX.len; 258 | 259 | const dev = try parseDevice(allocator, table.name.ptr[prefixlen..table.name.len], table); 260 | if (Verbose) { 261 | try out.print("add {s} to device list, with watch {s} and state {s} \n", .{ dev.name, dev.watchTopics, dev.stateTopics }); 262 | } 263 | _ = try _alldevices.put(dev.name, dev); 264 | } else { 265 | try out.print("bad prefix for section :{s} , only device_ or agent_ accepted, skipped \n", .{e.key_ptr}); 266 | } 267 | } 268 | }, 269 | toml.Value.None, toml.Value.String, toml.Value.Boolean, toml.Value.Integer, toml.Value.Float, toml.Value.Array, toml.Value.ManyTables => continue, 270 | } 271 | } 272 | 273 | const conf = try allocator.create(Config); 274 | 275 | if (config.keys.get("mqtt")) |mqttconfig| { 276 | if (mqttconfig.Table.keys.get("serverAddress")) |configAdd| { 277 | conf.mqttBroker = try allocator.alloc(u8, configAdd.String.len); 278 | mem.copy(u8, conf.mqttBroker, configAdd.String); 279 | } else { 280 | return error.noKeyServerAddress; 281 | } 282 | 283 | if (mqttconfig.Table.keys.get("user")) |u| { 284 | conf.user = try allocator.alloc(u8, u.String.len); 285 | mem.copy(u8, conf.user, u.String); 286 | } else { 287 | return error.ConfigNoUser; 288 | } 289 | 290 | if (mqttconfig.Table.keys.get("password")) |p| { 291 | conf.password = try allocator.alloc(u8, p.String.len); 292 | mem.copy(u8, conf.password, p.String); 293 | } else { 294 | return error.ConfigNoPassword; 295 | } 296 | 297 | if (mqttconfig.Table.keys.get("clientid")) |cid| { 298 | conf.clientid = try allocator.alloc(u8, cid.String.len); 299 | mem.copy(u8, conf.clientid, cid.String); 300 | 301 | try out.print("Using {s} as clientid \n", .{conf.clientid}); 302 | } else { 303 | conf.clientid = try allocator.alloc(u8, "iotmonitor".len); 304 | mem.copy(u8, conf.clientid, "iotmonitor"); 305 | } 306 | 307 | const topicBase = if (mqttconfig.Table.keys.get("baseTopic")) |baseTopic| baseTopic.String else "home/monitoring"; 308 | 309 | conf.mqttIotmonitorBaseTopic = try allocator.alloc(u8, topicBase.len + 1); 310 | conf.mqttIotmonitorBaseTopic[topicBase.len] = 0; 311 | mem.copy(u8, conf.mqttIotmonitorBaseTopic, topicBase[0..topicBase.len]); 312 | } else { 313 | return error.ConfigNoMqttSection; 314 | } 315 | 316 | const httpconf = try allocator.create(HttpServerConfig); 317 | 318 | if (config.keys.get("http")) |httpconfig| { 319 | httpconf.*.activateHttp = true; 320 | 321 | if (httpconfig.Table.keys.get("bind")) |baddr| { 322 | httpconf.listenAddress = try allocator.alloc(u8, baddr.String.len); 323 | mem.copy(u8, httpconf.listenAddress, baddr.String); 324 | } else { 325 | const localhostip = "127.0.0.1"; 326 | httpconf.listenAddress = try allocator.alloc(u8, localhostip.len); 327 | mem.copy(u8, httpconf.listenAddress, localhostip); 328 | } 329 | 330 | if (httpconfig.Table.keys.get("port")) |port| { 331 | httpconf.*.port = @intCast(u16, port.Integer); 332 | } else { 333 | httpconf.*.port = 8079; 334 | } 335 | } 336 | 337 | HttpConfig = httpconf; 338 | MqttConfig = conf; 339 | } 340 | 341 | // MQTT call back to handle the error handling and not 342 | // send the error to C paho library 343 | fn _external_callback(topic: []u8, message: []u8) void { 344 | callback(topic, message) catch { 345 | @panic("error in the callback"); 346 | }; 347 | } 348 | 349 | // MQTT Callback implementation 350 | fn callback(topic: []u8, message: []u8) !void { 351 | const t = tracy.trace(@src()); 352 | defer t.end(); 353 | 354 | // MQTT callback 355 | if (Verbose) { 356 | try out.print("on topic {s}\n", .{topic}); 357 | try out.print(" message arrived {s}\n", .{message}); 358 | try out.writeAll(topic); 359 | try out.writeAll("\n"); 360 | } 361 | 362 | // look for all devices 363 | var iterator = alldevices.iterator(); 364 | 365 | // device loop 366 | while (iterator.next()) |e| { 367 | const deviceInfo = e.value_ptr.*; 368 | if (Verbose) { 369 | if (deviceInfo.stateTopics) |stopic| { 370 | try out.print("evaluate state topic {s} with incoming topic {s} \n", .{ stopic, topic }); 371 | } 372 | } 373 | const watchTopic = deviceInfo.watchTopics; 374 | const storeTopic = deviceInfo.stateTopics; 375 | const helloTopic = deviceInfo.helloTopic; 376 | if (storeTopic) |store| { 377 | if (try topics.doesTopicBelongTo(topic, store)) |_| { 378 | 379 | // always store topic, even if the monitoring is not enabled 380 | 381 | // store sub topic in leveldb 382 | // trigger the refresh for timeout 383 | if (Verbose) { 384 | try out.print("sub topic to store value :{s}, in {s}\n", .{ message, topic }); 385 | try out.print("length {}\n", .{topic.len}); 386 | } 387 | db.put(topic, message) catch |errStorage| { 388 | log.warn("fail to store message {s} for topic {s}, on database with error {} \n", .{ message, topic, errStorage }); 389 | }; 390 | } 391 | } 392 | if (helloTopic) |hello| { 393 | if (mem.eql(u8, topic, hello)) { 394 | if (Verbose) { 395 | try out.print("device started, put all state informations \n", .{}); 396 | } 397 | // count the number of hello topic 398 | // 399 | // 400 | deviceInfo.helloTopicCount += 1; 401 | 402 | // iterate on db, on state topic 403 | 404 | const itstorage = try db.iterator(); 405 | // itstorage is an allocated pointer 406 | defer globalAllocator.destroy(itstorage); 407 | defer itstorage.deinit(); 408 | itstorage.first(); 409 | while (itstorage.isValid()) { 410 | var storedTopic = itstorage.iterKey(); 411 | if (storedTopic) |storedTopicValue| { 412 | defer globalAllocator.destroy(storedTopicValue); 413 | if (storedTopicValue.len >= topic.len) { 414 | const slice = storedTopicValue.*; 415 | if (mem.eql(u8, slice[0..topic.len], topic[0..])) { 416 | if (deviceInfo.enabled) { 417 | // send the state only if the monitoring is enabled 418 | 419 | var stateTopic = itstorage.iterValue(); 420 | if (stateTopic) |stateTopicValue| { 421 | if (Verbose) { 422 | try out.print("sending state {s} to topic {s}\n", .{ stateTopic.?.*, slice }); 423 | } 424 | defer globalAllocator.destroy(stateTopicValue); 425 | const topicWithSentinel = try globalAllocator.allocSentinel(u8, storedTopicValue.*.len, 0); 426 | defer globalAllocator.free(topicWithSentinel); 427 | mem.copy(u8, topicWithSentinel[0..], storedTopicValue.*); 428 | 429 | // resend state 430 | cnx.publish(topicWithSentinel, stateTopicValue.*) catch |errorMqtt| { 431 | log.warn("ERROR {} fail to publish initial state on topic {}", .{ errorMqtt, topicWithSentinel }); 432 | try out.print(".. state restoring done, listening mqtt topics\n", .{}); 433 | }; 434 | } 435 | } 436 | } 437 | } 438 | } 439 | itstorage.next(); 440 | } 441 | } 442 | } // hello 443 | if (try topics.doesTopicBelongTo(topic, watchTopic)) |_| { 444 | // trigger the timeout for the iot element 445 | try deviceInfo.updateNextContact(); 446 | } 447 | } 448 | 449 | if (Verbose) { 450 | try out.print("end of callback \n", .{}); 451 | } 452 | } 453 | 454 | // global types 455 | const AllDevices = std.StringHashMap(*MonitoringInfo); 456 | const DiskHash = leveldb.LevelDBHashArray(u8, u8); 457 | 458 | // global variables 459 | var globalAllocator = std.heap.raw_c_allocator; 460 | var alldevices: AllDevices = undefined; 461 | var db: *DiskHash = undefined; 462 | 463 | test "read whole database" { 464 | db = try DiskHash.init(&globalAllocator); 465 | const filename = "iotdb.leveldb"; 466 | _ = try db.open(filename); 467 | defer db.close(); 468 | 469 | const iterator = try db.iterator(); 470 | defer globalAllocator.destroy(iterator); 471 | defer iterator.deinit(); 472 | log.warn("Dump the iot database \n", .{}); 473 | iterator.first(); 474 | while (iterator.isValid()) { 475 | const optReadKey = iterator.iterKey(); 476 | if (optReadKey) |k| { 477 | defer globalAllocator.destroy(k); 478 | const optReadValue = iterator.iterValue(); 479 | if (optReadValue) |v| { 480 | log.warn(" key :{} value: {}\n", .{ k.*, v.* }); 481 | defer globalAllocator.destroy(v); 482 | } 483 | } 484 | iterator.next(); 485 | } 486 | } 487 | // main connection for subscription 488 | var cnx: *mqtt.MqttCnx = undefined; 489 | var cpt: u32 = 0; 490 | 491 | const MAGICPROCSSHEADER = "IOTMONITORMAGIC_"; 492 | const MAGIC_BUFFER_SIZE = 16 * 1024; 493 | const LAUNCH_COMMAND_LINE_BUFFER_SIZE = 16 * 1024; 494 | 495 | fn launchProcess(monitoringInfo: *MonitoringInfo) !void { 496 | assert(monitoringInfo.associatedProcessInformation != null); 497 | const associatedProcessInformation = monitoringInfo.*.associatedProcessInformation.?.*; 498 | const pid = try os.fork(); 499 | if (pid == 0) { 500 | // detach from parent, this permit the process to live independently from 501 | // its parent 502 | _ = c.setsid(); 503 | 504 | const bufferMagic = try globalAllocator.allocSentinel(u8, MAGIC_BUFFER_SIZE, 0); 505 | defer globalAllocator.free(bufferMagic); 506 | _ = c.sprintf(bufferMagic.ptr, "%s%s", MAGICPROCSSHEADER, monitoringInfo.name.ptr); 507 | 508 | const commandLineBuffer = try globalAllocator.allocSentinel(u8, LAUNCH_COMMAND_LINE_BUFFER_SIZE, 0); 509 | defer globalAllocator.free(commandLineBuffer); 510 | 511 | const exec = associatedProcessInformation.exec; 512 | _ = c.sprintf(commandLineBuffer.ptr, "echo %s;%s;echo END", bufferMagic.ptr, exec.ptr); 513 | 514 | // launch here a bash to have a silent process identification 515 | const argv = [_][]const u8{ 516 | "/bin/bash", 517 | "-c", 518 | commandLineBuffer[0..c.strlen(commandLineBuffer)], 519 | }; 520 | 521 | var m = std.BufMap.init(globalAllocator); 522 | // may add additional information about the process ... 523 | try m.put("IOTMONITORMAGIC", bufferMagic[0..c.strlen(bufferMagic)]); 524 | 525 | // execute the process 526 | std.process.execve(globalAllocator, &argv, &m) catch { 527 | unreachable; 528 | }; 529 | 530 | // if succeeded the process is replaced 531 | 532 | } else { 533 | try out.print("process launched, pid : {}\n", .{pid}); 534 | monitoringInfo.*.associatedProcessInformation.?.pid = pid; 535 | monitoringInfo.*.associatedProcessInformation.?.restartedCount += 1; 536 | _ = c.time(&monitoringInfo.*.associatedProcessInformation.?.lastRestarted); 537 | 538 | // launch mqtt restart information process in monitoring 539 | try publishProcessStarted(monitoringInfo); 540 | } 541 | } 542 | 543 | test "test_launch_process" { 544 | globalAllocator = std.heap.c_allocator; 545 | alldevices = AllDevices.init(globalAllocator); 546 | 547 | var processInfo = AdditionalProcessInformation{ 548 | .exec = "sleep 20", 549 | .pid = undefined, 550 | }; 551 | var d = MonitoringInfo{ 552 | .timeoutValue = 1, 553 | .name = "MYPROCESS", 554 | .watchTopics = "", 555 | .nextContact = undefined, 556 | .allocator = undefined, 557 | .helloTopic = undefined, 558 | .stateTopics = undefined, 559 | .associatedProcessInformation = &processInfo, 560 | }; 561 | 562 | // try launchProcess(&d); 563 | // const pid: i32 = d.associatedProcessInformation.?.*.pid.?; 564 | // debug.warn("pid launched : {}\n", .{pid}); 565 | 566 | try alldevices.put(d.name, &d); 567 | 568 | // var p: processlib.ProcessInformation = .{}; 569 | // const processFound = try processlib.getProcessInformations(pid, &p); 570 | // assert(processFound); 571 | 572 | try processlib.listProcesses(handleCheckAgent); 573 | } 574 | 575 | fn handleCheckAgent(processInformation: *processlib.ProcessInformation) void { 576 | // iterate over the devices, to check which device belong to this process 577 | // information 578 | var it = alldevices.iterator(); 579 | 580 | while (it.next()) |deviceInfo| { 581 | const device = deviceInfo.value_ptr.*; 582 | // not on optional 583 | if (device.associatedProcessInformation) |infos| { 584 | // check if process has the magic Key in the process list 585 | var itCmdLine = processInformation.iterator(); 586 | while (itCmdLine.next()) |a| { 587 | if (Verbose) { 588 | out.print("look in {s}\n", .{a}) catch unreachable; 589 | } 590 | 591 | const bufferMagic = globalAllocator.allocSentinel(u8, MAGIC_BUFFER_SIZE, 0) catch unreachable; 592 | defer globalAllocator.free(bufferMagic); 593 | _ = c.sprintf(bufferMagic.ptr, "%s%s", MAGICPROCSSHEADER, device.name.ptr); 594 | 595 | const p = c.strstr(a.ptr, bufferMagic.ptr); 596 | if (Verbose) { 597 | out.print("found {*}\n", .{p}) catch unreachable; 598 | } 599 | if (p != null) { 600 | // found in arguments, remember the pid 601 | // of the process 602 | infos.*.pid = processInformation.*.pid; 603 | if (Verbose) { 604 | out.print("process {} is monitored pid found\n", .{infos.pid}) catch unreachable; 605 | } 606 | break; 607 | } 608 | if (Verbose) { 609 | out.writeAll("next ..\n") catch unreachable; 610 | } 611 | } 612 | } else { 613 | continue; 614 | } 615 | } 616 | } 617 | 618 | fn runAllMissings() !void { 619 | // once all the process have been browsed, 620 | // run all missing processes 621 | 622 | var it = alldevices.iterator(); 623 | while (it.next()) |deviceinfo| { 624 | const device = deviceinfo.value_ptr.*; 625 | if (device.associatedProcessInformation) |processinfo| { 626 | // this is a process monitored 627 | if (device.enabled) { 628 | if (processinfo.*.pid == null) { 629 | out.print("running ...{s} \n", .{device.name}) catch unreachable; 630 | // no pid associated to the info 631 | // 632 | launchProcess(device) catch { 633 | @panic("fail to run process"); 634 | }; 635 | } 636 | } else { 637 | // monitoring not enabled on the process 638 | 639 | } 640 | } 641 | } 642 | } 643 | 644 | fn checkProcessesAndRunMissing() !void { 645 | const t = tracy.trace(@src()); 646 | defer t.end(); 647 | 648 | // RAZ pid infos 649 | var it = alldevices.iterator(); 650 | 651 | while (it.next()) |deviceInfo| { 652 | const device = deviceInfo.value_ptr.*; 653 | if (device.associatedProcessInformation) |infos| { 654 | infos.pid = null; 655 | } 656 | } 657 | // list all process for wrapping 658 | try processlib.listProcesses(handleCheckAgent); 659 | try runAllMissings(); 660 | } 661 | 662 | // this function publish a watchdog for the iotmonitor process 663 | // this permit to check if the monitoring is up 664 | fn publishWatchDog() !void { 665 | const t = tracy.trace(@src()); 666 | defer t.end(); 667 | 668 | var topicBufferPayload = try globalAllocator.alloc(u8, 512); 669 | defer globalAllocator.free(topicBufferPayload); 670 | secureZero(topicBufferPayload); 671 | _ = c.sprintf(topicBufferPayload.ptr, "%s/up", MqttConfig.mqttIotmonitorBaseTopic.ptr); 672 | 673 | var bufferPayload = try globalAllocator.alloc(u8, 512); 674 | defer globalAllocator.free(bufferPayload); 675 | secureZero(bufferPayload); 676 | cpt = (cpt + 1) % 1_000_000; 677 | _ = c.sprintf(bufferPayload.ptr, "%d", cpt); 678 | 679 | const payloadLength = c.strlen(bufferPayload.ptr); 680 | 681 | cnx.publish(topicBufferPayload.ptr, bufferPayload[0..payloadLength]) catch { 682 | log.warn("cannot publish watchdog message, will retryi \n", .{}); 683 | }; 684 | } 685 | 686 | fn publishProcessStarted(mi: *MonitoringInfo) !void { 687 | const t = tracy.trace(@src()); 688 | defer t.end(); 689 | 690 | var topicBufferPayload = try globalAllocator.alloc(u8, 512); 691 | defer globalAllocator.free(topicBufferPayload); 692 | secureZero(topicBufferPayload); 693 | _ = c.sprintf(topicBufferPayload.ptr, "%s/startedprocess/%s", MqttConfig.mqttIotmonitorBaseTopic.ptr, mi.name.ptr); 694 | 695 | var bufferPayload = try globalAllocator.alloc(u8, 512); 696 | defer globalAllocator.free(bufferPayload); 697 | secureZero(bufferPayload); 698 | _ = c.sprintf(bufferPayload.ptr, "%d", mi.*.associatedProcessInformation.?.lastRestarted); 699 | 700 | const payloadLength = c.strlen(bufferPayload.ptr); 701 | cnx.publish(topicBufferPayload.ptr, bufferPayload[0..payloadLength]) catch { 702 | log.warn("cannot publish watchdog message, will retryi \n", .{}); 703 | }; 704 | } 705 | 706 | // this publish on the mqtt broker the process informations 707 | // 708 | fn publishDeviceMonitoringInfos(device: *MonitoringInfo) !void { 709 | var topicBufferPayload = try globalAllocator.alloc(u8, 512); 710 | defer globalAllocator.free(topicBufferPayload); 711 | secureZero(topicBufferPayload); 712 | _ = c.sprintf(topicBufferPayload.ptr, "%s/helloTopicCount/%s", MqttConfig.mqttIotmonitorBaseTopic.ptr, device.*.name.ptr); 713 | 714 | var bufferPayload = try globalAllocator.alloc(u8, 512); 715 | defer globalAllocator.free(bufferPayload); 716 | secureZero(bufferPayload); 717 | 718 | _ = c.sprintf(bufferPayload.ptr, "%u", device.helloTopicCount); 719 | const payloadLen = c.strlen(bufferPayload.ptr); 720 | 721 | cnx.publish(topicBufferPayload.ptr, bufferPayload[0..payloadLen]) catch { 722 | log.warn("cannot publish timeout message for device {} , will retry \n", .{device.name}); 723 | }; 724 | } 725 | 726 | // this function pulish a mqtt message for a device that is not publishing 727 | // it mqtt messages 728 | fn publishDeviceTimeOut(device: *MonitoringInfo) !void { 729 | var topicBufferPayload = try globalAllocator.alloc(u8, 512); 730 | defer globalAllocator.free(topicBufferPayload); 731 | secureZero(topicBufferPayload); 732 | _ = c.sprintf(topicBufferPayload.ptr, "%s/expired/%s", MqttConfig.mqttIotmonitorBaseTopic.ptr, device.*.name.ptr); 733 | 734 | var bufferPayload = try globalAllocator.alloc(u8, 512); 735 | defer globalAllocator.free(bufferPayload); 736 | 737 | secureZero(bufferPayload); 738 | _ = c.sprintf(bufferPayload.ptr, "%d", device.nextContact); 739 | const payloadLen = c.strlen(bufferPayload.ptr); 740 | 741 | cnx.publish(topicBufferPayload.ptr, bufferPayload[0..payloadLen]) catch { 742 | log.warn("cannot publish timeout message for device {} , will retry \n", .{device.name}); 743 | }; 744 | } 745 | 746 | const JSONStatus = struct { 747 | name: []const u8, 748 | enabled: bool = true, 749 | expired: bool, 750 | }; 751 | 752 | fn indexHandler(req: Request, res: Response) !void { 753 | _ = req; 754 | const t = tracy.trace(@src()); 755 | defer t.end(); 756 | 757 | var iterator = alldevices.iterator(); 758 | try res.setType("application/json"); 759 | try res.body.writeAll("["); 760 | var hasone = false; 761 | while (iterator.next()) |e| { 762 | const deviceInfo = e.value_ptr.*; 763 | if (hasone) { 764 | try res.body.writeAll(","); 765 | } 766 | const j = JSONStatus{ .name = deviceInfo.name[0 .. deviceInfo.name.len - 1], .enabled = deviceInfo.enabled, .expired = try deviceInfo.hasExpired() }; 767 | // create a json response associated 768 | 769 | try json.stringify(j, json.StringifyOptions{}, res.body); 770 | hasone = true; 771 | } 772 | try res.body.writeAll("]"); 773 | // try res.write("IotMonitor version 0.2.2"); 774 | } 775 | 776 | const Address = std.net.Address; 777 | const routez = @import("routez"); 778 | const Request = routez.Request; 779 | const Response = routez.Response; 780 | const Server = routez.Server; 781 | 782 | const Thread = std.Thread; 783 | 784 | var server: Server = undefined; 785 | var addr: Address = undefined; 786 | 787 | // http server context 788 | const ServerCtx = struct {}; 789 | pub fn startServer(context: ServerCtx) void { 790 | _ = context; 791 | server.listen(addr) catch { 792 | @panic("cannot start listening http server"); 793 | }; 794 | } 795 | 796 | // main procedure 797 | pub fn main() !void { 798 | const params = comptime [_]clap.Param(clap.Help){ 799 | clap.parseParam("-h, --help Display this help") catch unreachable, 800 | clap.parseParam("-v, --version Display version") catch unreachable, 801 | clap.parseParam("...") catch unreachable, 802 | }; 803 | 804 | var diag = clap.Diagnostic{}; 805 | 806 | var args = clap.parse(clap.Help, ¶ms, .{ .diagnostic = &diag }) catch |err| { 807 | // Report useful error and exit 808 | diag.report(io.getStdErr().writer(), err) catch {}; 809 | return err; 810 | }; 811 | defer args.deinit(); 812 | 813 | if (args.flag("--help")) { 814 | debug.print("\n", .{}); 815 | debug.print("start the iotmonitor deamon, usage :\n", .{}); 816 | debug.print(" iotmonitor [optional config.toml filepath]\n", .{}); 817 | debug.print("\n", .{}); 818 | return; 819 | } 820 | 821 | if (args.flag("--version")) { 822 | debug.print("{s}", .{version.version}); 823 | return; 824 | } 825 | 826 | try out.writeAll("IotMonitor start, version "); 827 | try out.writeAll(version.version); 828 | try out.writeAll("\n"); 829 | 830 | // default 831 | const defaultConfigFile = "config.toml"; 832 | var configurationFile = try globalAllocator.alloc(u8, defaultConfigFile.len); 833 | mem.copy(u8, configurationFile, defaultConfigFile); 834 | var arg_index: u32 = 0; 835 | for (args.positionals()) |pos| { 836 | debug.print("{s}\n", .{pos}); 837 | debug.print("{}\n", .{pos.len}); 838 | if (arg_index == 0) { 839 | globalAllocator.free(configurationFile); 840 | configurationFile = try globalAllocator.alloc(u8, pos.len); 841 | mem.copy(u8, configurationFile, pos); 842 | } 843 | arg_index += 1; 844 | } 845 | 846 | try out.writeAll("Reading "); 847 | try out.writeAll(configurationFile); 848 | try out.writeAll(" file\n"); 849 | 850 | // Friendly error if the file does not exists 851 | var openedtestfile = std.os.open(configurationFile, 0, 0) catch { 852 | try out.writeAll("Cannot open file "); 853 | try out.writeAll(configurationFile); 854 | try out.writeAll("\n"); 855 | return; 856 | }; 857 | std.os.close(openedtestfile); 858 | 859 | alldevices = AllDevices.init(globalAllocator); 860 | try parseTomlConfig(globalAllocator, &alldevices, configurationFile); 861 | 862 | try out.writeAll("Opening database\n"); 863 | 864 | db = try DiskHash.init(&globalAllocator); 865 | const filename = "iotdb.leveldb"; 866 | _ = try db.open(filename); 867 | defer db.close(); 868 | 869 | // connecting to MQTT 870 | 871 | var serverAddress: []const u8 = MqttConfig.mqttBroker; 872 | var userName: []const u8 = MqttConfig.user; 873 | var password: []const u8 = MqttConfig.password; 874 | var clientid: []const u8 = MqttConfig.clientid; 875 | 876 | try out.writeAll("Connecting to mqtt ..\n"); 877 | 878 | try out.print(" connecting to \"{s}\" with user \"{s}\" and clientid \"{s}\"\n", .{ serverAddress, userName, clientid }); 879 | 880 | cnx = try mqtt.MqttCnx.init(&globalAllocator, serverAddress, clientid, userName, password); 881 | 882 | if (HttpConfig.activateHttp) { 883 | try out.print("Start embedded http server on port {} \n", .{HttpConfig.*.port}); 884 | server = Server.init( 885 | globalAllocator, 886 | .{}, 887 | .{routez.all("/", indexHandler)}, 888 | ); 889 | addr = try Address.parseIp4(HttpConfig.*.listenAddress, HttpConfig.*.port); 890 | 891 | const threadConfig: Thread.SpawnConfig = .{}; 892 | const threadHandle = try Thread.spawn(threadConfig, startServer, .{.{}}); 893 | _ = threadHandle; 894 | try out.print("Http server thread launched\n", .{}); 895 | } 896 | 897 | try out.print("Checking running monitored processes\n", .{}); 898 | try checkProcessesAndRunMissing(); 899 | 900 | try out.print("Restoring saved states topics ... \n", .{}); 901 | // read all elements in database, then redefine the state for all 902 | const it = try db.iterator(); 903 | defer globalAllocator.destroy(it); 904 | defer it.deinit(); 905 | 906 | it.first(); 907 | while (it.isValid()) { 908 | const r = it.iterKey(); 909 | if (r) |subject| { 910 | defer globalAllocator.destroy(subject); 911 | const v = it.iterValue(); 912 | if (v) |value| { 913 | defer globalAllocator.destroy(value); 914 | try out.print("Sending initial stored state {s} to {s}\n", .{ value.*, subject.* }); 915 | 916 | const topicWithSentinel = try globalAllocator.allocSentinel(u8, subject.*.len, 0); 917 | defer globalAllocator.free(topicWithSentinel); 918 | mem.copy(u8, topicWithSentinel[0..], subject.*); 919 | 920 | // if failed, stop the process 921 | cnx.publish(topicWithSentinel, value.*) catch |e| { 922 | log.warn("ERROR {} fail to publish initial state on topic {s}", .{ e, topicWithSentinel }); 923 | try out.print(".. State restoring done, listening mqtt topics\n", .{}); 924 | }; 925 | } 926 | } 927 | it.next(); 928 | } 929 | try out.print(".. State restoring done, listening mqtt topics\n", .{}); 930 | cnx.callBack = _external_callback; 931 | 932 | // register to all, it may be huge, and probably not scaling 933 | _ = try cnx.register("#"); 934 | 935 | while (true) { // main loop 936 | _ = c.sleep(1); // every 1 seconds 937 | 938 | { 939 | // if activated trace this function 940 | const t = tracy.trace(@src()); 941 | defer t.end(); 942 | 943 | // check process that has falled down, and must be restarted 944 | try checkProcessesAndRunMissing(); 945 | // watchdog 946 | try publishWatchDog(); 947 | 948 | var iterator = alldevices.iterator(); 949 | while (iterator.next()) |e| { 950 | // publish message 951 | const deviceInfo = e.value_ptr.*; 952 | 953 | if (deviceInfo.enabled) { 954 | // if the device is enabled 955 | const hasExpired = try deviceInfo.hasExpired(); 956 | if (hasExpired) { 957 | try publishDeviceTimeOut(deviceInfo); 958 | } 959 | try publishDeviceMonitoringInfos(deviceInfo); 960 | } 961 | } 962 | } 963 | } 964 | 965 | log.warn("ended", .{}); 966 | return; 967 | } 968 | -------------------------------------------------------------------------------- /leveldb.zig: -------------------------------------------------------------------------------- 1 | // This library is a thick binding to leveldb C API 2 | 3 | const std = @import("std"); 4 | const builtin = @import("builtin"); 5 | const trait = std.meta.trait; 6 | const debug = std.debug; 7 | const log = std.log; 8 | const assert = debug.assert; 9 | const Allocator = mem.Allocator; 10 | const mem = std.mem; 11 | 12 | const cleveldb = @cImport({ 13 | @cInclude("leveldb/c.h"); 14 | }); 15 | const c = @cImport({ 16 | @cInclude("stdio.h"); 17 | @cInclude("unistd.h"); 18 | }); 19 | 20 | /////////////////////////////////////////////////////////////////////// 21 | // Serialization / Deserialisation 22 | 23 | // this define the generic API for either Simple Type and composite Type for serialization 24 | fn SerDeserAPI(comptime T: type, comptime INType: type, comptime OUTType: type) type { 25 | _ = T; 26 | return struct { 27 | const MarshallFn = fn (*const INType, *Allocator) []const u8; 28 | const UnMarshallFn = fn ([]const u8, *Allocator) *align(1) OUTType; 29 | marshall: MarshallFn, 30 | unMarshall: UnMarshallFn, 31 | }; 32 | } 33 | 34 | pub fn ArrSerDeserType(comptime T: type) SerDeserAPI(T, []const T, []T) { 35 | const MI = struct { 36 | // default functions when the type is still serializable 37 | fn noMarshallFn(t: *const []const T, allocator: *Allocator) []const u8 { 38 | _ = allocator; 39 | return t.*; 40 | } 41 | 42 | // unmarshall must make a copy, 43 | // because leveldb has its own buffers 44 | fn noUnMarshallFn(e: []const u8, allocator: *Allocator) *align(1) []T { 45 | const ptrContainer = @ptrToInt(&e); 46 | const elements = @intToPtr(*align(1) []T, ptrContainer); 47 | const pNew = allocator.create([]T) catch unreachable; 48 | pNew.* = elements.*; 49 | return pNew; 50 | } 51 | }; 52 | 53 | const S = SerDeserAPI(T, []const T, []T); 54 | return S{ 55 | .marshall = MI.noMarshallFn, 56 | .unMarshall = MI.noUnMarshallFn, 57 | }; 58 | } 59 | // need sed deser for storing on disk 60 | pub fn SerDeserType(comptime T: type) SerDeserAPI(T, T, T) { 61 | comptime { 62 | if (@typeInfo(T) == .Pointer) { 63 | @panic("this ser deser type is only for simple types"); 64 | } 65 | } 66 | 67 | const MI = struct { 68 | // default functions when the type is still serializable 69 | fn noMarshallFn(t: *const T, allocator: *Allocator) []const u8 { 70 | _ = allocator; 71 | return mem.asBytes(t); 72 | } 73 | 74 | // unmarshall must make a copy, 75 | // because leveldb has its own buffers 76 | fn noUnMarshallFn(e: []const u8, allocator: *Allocator) *align(1) T { 77 | _ = allocator; 78 | const eptr = @ptrToInt(&e[0]); 79 | return @intToPtr(*align(1) T, eptr); 80 | } 81 | }; 82 | 83 | const S = SerDeserAPI(T, T, T); 84 | return S{ 85 | .marshall = MI.noMarshallFn, 86 | .unMarshall = MI.noUnMarshallFn, 87 | }; 88 | } 89 | 90 | test "use of noMarshallFn" { 91 | var allocator = std.heap.c_allocator; 92 | const i: u32 = 12; 93 | var r = SerDeserType(u32).marshall(&i, &allocator); 94 | const ur = SerDeserType(u32).unMarshall(r, &allocator); 95 | debug.assert(i == ur.*); 96 | } 97 | 98 | // create level db type for single element, 99 | pub fn LevelDBHash(comptime K: type, comptime V: type) type { 100 | return LevelDBHashWithSerialization(K, K, K, V, V, V, SerDeserType(K), SerDeserType(V)); 101 | } 102 | 103 | // create a leveldb type for arrays of element, for instance 104 | // []u8 (strings) 105 | pub fn LevelDBHashArray(comptime K: type, comptime V: type) type { 106 | return LevelDBHashWithSerialization(K, []const K, []K, V, []const V, []V, ArrSerDeserType(K), ArrSerDeserType(V)); 107 | } 108 | 109 | // This function create a given type for using the leveldb, with serialization 110 | // and deserialization 111 | pub fn LevelDBHashWithSerialization( 112 | // K is the key type, 113 | comptime K: type, 114 | // KIN is the associated type used in "put" primitive (when the element is passed in) 115 | comptime KIN: type, 116 | // KOUT is the associated type for out, see get primitive 117 | comptime KOUT: type, 118 | // V is the value base type 119 | comptime V: type, 120 | comptime VIN: type, 121 | comptime VOUT: type, 122 | // marshallkey function serialize the k to be stored in the database 123 | comptime marshallKey: SerDeserAPI(K, KIN, KOUT), 124 | comptime marshallValue: SerDeserAPI(V, VIN, VOUT), 125 | ) type { 126 | return struct { 127 | leveldbHandle: *cleveldb.leveldb_t = undefined, 128 | writeOptions: *cleveldb.leveldb_writeoptions_t = undefined, 129 | readOptions: *cleveldb.leveldb_readoptions_t = undefined, 130 | allocator: *Allocator, 131 | 132 | const Self = @This(); 133 | const KSerDeser = marshallKey; 134 | const VSerDeser = marshallValue; 135 | 136 | pub fn init(allocator: *Allocator) !*Self { 137 | const self = try allocator.create(Self); 138 | self.allocator = allocator; 139 | self.writeOptions = cleveldb.leveldb_writeoptions_create().?; 140 | self.readOptions = cleveldb.leveldb_readoptions_create().?; 141 | return self; 142 | } 143 | 144 | pub fn deinit(self: *Self) void { 145 | debug.print("deinit \n", .{}); 146 | // close already free the leveldb handle 147 | // cleveldb.leveldb_free(self.leveldbHandle); 148 | cleveldb.leveldb_free(self.writeOptions); 149 | cleveldb.leveldb_free(self.readOptions); 150 | } 151 | 152 | pub fn open(self: *Self, filename: [*c]const u8) !void { 153 | const options = cleveldb.leveldb_options_create(); 154 | defer cleveldb.leveldb_free(options); 155 | 156 | // defining a small LRU cache, 157 | // on level db, initialy initialized to 32Gb with 4096 bock size 158 | // because, the key replace, will keep the values until compaction 159 | // to full iteration can get all datas and put them into LRU cache 160 | 161 | const cache = cleveldb.leveldb_cache_create_lru(4096); 162 | const env = cleveldb.leveldb_create_default_env(); 163 | cleveldb.leveldb_options_set_cache(options, cache); 164 | cleveldb.leveldb_options_set_create_if_missing(options, 1); 165 | cleveldb.leveldb_options_set_max_open_files(options, 10); 166 | cleveldb.leveldb_options_set_block_restart_interval(options, 4); 167 | cleveldb.leveldb_options_set_write_buffer_size(options, 1000); 168 | cleveldb.leveldb_options_set_env(options, env); 169 | cleveldb.leveldb_options_set_info_log(options, null); 170 | cleveldb.leveldb_options_set_block_size(options, 1024); 171 | 172 | var err: [*c]u8 = null; 173 | const result = cleveldb.leveldb_open(options, filename, &err); 174 | 175 | if (err != null) { 176 | _ = log.warn("{s}", .{"open failed"}); 177 | defer cleveldb.leveldb_free(err); 178 | return error.OPEN_FAILED; 179 | } 180 | assert(result != null); 181 | self.leveldbHandle = result.?; 182 | } 183 | 184 | pub fn close(self: *Self) void { 185 | log.warn("close database \n", .{}); 186 | cleveldb.leveldb_close(self.leveldbHandle); 187 | self.leveldbHandle = undefined; 188 | } 189 | 190 | pub fn put(self: *Self, key: KIN, value: VIN) !void { 191 | var err: [*c]u8 = null; 192 | // debug.print("k size {}\n", .{@sizeOf(K)}); 193 | // debug.print("array size {}\n", .{value.len}); 194 | 195 | // marshall value 196 | const marshalledKey = KSerDeser.marshall(&key, self.allocator); 197 | // defer self.allocator.free(marshalledKey); 198 | const marshalledValue = VSerDeser.marshall(&value, self.allocator); 199 | // defer self.allocator.free(marshalledValue); 200 | cleveldb.leveldb_put(self.leveldbHandle, self.writeOptions, marshalledKey.ptr, marshalledKey.len, marshalledValue.ptr, marshalledValue.len, &err); 201 | if (err != null) { 202 | log.warn("{s}", .{"open failed"}); 203 | defer cleveldb.leveldb_free(err); 204 | return error.KEY_WRITE_FAILED; 205 | } 206 | } 207 | 208 | // retrieve the content of a key 209 | pub fn get(self: *Self, key: KIN) !?*align(1) VOUT { 210 | var read: ?[*]u8 = undefined; 211 | var read_len: usize = 0; 212 | var err: [*c]u8 = null; 213 | const marshalledKey = KSerDeser.marshall(&key, self.allocator); 214 | // defer self.allocator.free(marshalledKey); 215 | read = cleveldb.leveldb_get(self.leveldbHandle, self.readOptions, marshalledKey.ptr, marshalledKey.len, &read_len, &err); 216 | 217 | if (err != null) { 218 | _ = c.printf("open failed"); 219 | defer cleveldb.leveldb_free(err); 220 | return null; 221 | } 222 | if (read == null) { 223 | debug.print("key not found \n", .{}); 224 | return null; 225 | } 226 | const torelease = @ptrCast(*anyopaque, read); 227 | defer cleveldb.leveldb_free(torelease); 228 | // _ = c.printf("returned : %s %d\n", read, read_len); 229 | const structAddr = @ptrToInt(read); 230 | var cmsg = @intToPtr([*]u8, structAddr); 231 | const vTypePtr = VSerDeser.unMarshall(cmsg[0..read_len], self.allocator); 232 | //return &cmsg[0..read_len]; 233 | return vTypePtr; 234 | } 235 | 236 | // iterator structure 237 | const Iterator = struct { 238 | const ItSelf = @This(); 239 | db: *Self = undefined, 240 | innerIt: *cleveldb.leveldb_iterator_t = undefined, 241 | allocator: *Allocator = undefined, 242 | const ConstIter_t = *const cleveldb.leveldb_iterator_t; 243 | 244 | fn init(allocator: *Allocator, db: *Self) !*Iterator { 245 | const obj = try allocator.create(Iterator); 246 | obj.allocator = allocator; 247 | const result = cleveldb.leveldb_create_iterator(db.leveldbHandle, db.readOptions); 248 | if (result) |innerhandle| { 249 | obj.innerIt = innerhandle; 250 | return obj; 251 | } 252 | return error.CannotCreateIterator; 253 | } 254 | 255 | pub fn first(self: *ItSelf) void { 256 | cleveldb.leveldb_iter_seek_to_first(self.innerIt); 257 | } 258 | 259 | /// iterKey retrieve the value of the iterator 260 | /// the memory on the value has been allocated and needs to be freed if not used 261 | pub fn iterKey(self: *ItSelf) ?*align(1) KOUT { 262 | var read_len: usize = 0; 263 | const read: ?[*c]const u8 = cleveldb.leveldb_iter_key(self.innerIt, &read_len); 264 | 265 | if (read) |existValue| { 266 | const structAddr = @ptrToInt(existValue); 267 | var cmsg = @intToPtr([*]const u8, structAddr); 268 | const spanRead = mem.bytesAsSlice(u8, cmsg[0..read_len]); 269 | const vTypePtr = KSerDeser.unMarshall(spanRead, self.allocator); 270 | return vTypePtr; 271 | } 272 | return null; 273 | } 274 | 275 | /// iterValue retrieve the value of the iterator 276 | /// the memory on the value has been allocated and needs to be freed if not used 277 | pub fn iterValue(self: *ItSelf) ?*align(1) VOUT { 278 | var read_len: usize = 0; 279 | const read: ?[*c]const u8 = cleveldb.leveldb_iter_value(self.innerIt, &read_len); 280 | 281 | if (read) |existValue| { 282 | const structAddr = @ptrToInt(existValue); 283 | var cmsg = @intToPtr([*]const u8, structAddr); 284 | const spanRead = mem.bytesAsSlice(u8, cmsg[0..read_len]); 285 | const vTypePtr = VSerDeser.unMarshall(spanRead, self.allocator); 286 | return vTypePtr; 287 | } 288 | return null; 289 | } 290 | 291 | pub fn next(self: *ItSelf) void { 292 | cleveldb.leveldb_iter_next(self.innerIt); 293 | } 294 | 295 | pub fn isValid(self: *ItSelf) bool { 296 | const result = cleveldb.leveldb_iter_valid(self.innerIt); 297 | return result > 0; 298 | } 299 | 300 | pub fn deinit(self: *ItSelf) void { 301 | cleveldb.leveldb_iter_destroy(self.innerIt); 302 | } 303 | }; 304 | 305 | pub fn iterator(self: *Self) !*Iterator { 306 | return Iterator.init(self.allocator, self); 307 | } 308 | }; 309 | } 310 | 311 | test "test iterators" { 312 | // already created database 313 | var filename = "countingstorage\x00"; 314 | 315 | var allocator = std.heap.c_allocator; 316 | 317 | const SS = LevelDBHash(u32, u32); 318 | var l = try SS.init(&allocator); 319 | defer l.deinit(); 320 | debug.print("opening databse", .{}); 321 | try l.open(filename); 322 | defer l.close(); 323 | 324 | debug.print("create iterator", .{}); 325 | const iterator = try l.iterator(); 326 | defer iterator.deinit(); 327 | 328 | debug.print("call first", .{}); 329 | iterator.first(); 330 | var r: ?*align(1) u32 = null; 331 | while (iterator.isValid()) { 332 | debug.print("iterKey", .{}); 333 | r = iterator.iterKey(); 334 | var v = iterator.iterValue(); 335 | debug.print("key :{} value: {}\n", .{ r.?.*, v.?.* }); 336 | allocator.destroy(v.?); 337 | iterator.next(); 338 | } 339 | debug.print("now, close the iterator\n", .{}); 340 | } 341 | 342 | test "test no specialization" { 343 | 344 | var allocator = std.heap.c_allocator; 345 | 346 | const SS = LevelDBHash(u32, u8); 347 | _ = try SS.init(&allocator); 348 | 349 | //var l = try SS.init(&allocator); 350 | // assert(l != null); 351 | } 352 | 353 | test "test storing ints" { 354 | var filename = "countingstorage\x00"; 355 | 356 | var allocator = std.heap.c_allocator; 357 | 358 | const SS = LevelDBHash(u32, u32); 359 | var l = try SS.init(&allocator); 360 | defer l.deinit(); 361 | 362 | _ = try l.open(filename); 363 | 364 | var i: u32 = 0; 365 | while (i < 1000) { 366 | try l.put(i, i + 10); 367 | i += 1; 368 | } 369 | l.close(); 370 | } 371 | 372 | test "test storing letters" { 373 | var filename = "stringstoragetest\x00"; 374 | 375 | var allocator = std.heap.c_allocator; 376 | 377 | const SS = LevelDBHashArray(u8, u8); 378 | var l = try SS.init(&allocator); 379 | defer l.deinit(); 380 | 381 | _ = try l.open(filename); 382 | 383 | const MAX_ITERATIONS = 100_000; 384 | 385 | var i: u64 = 0; 386 | while (i < MAX_ITERATIONS) { 387 | var keyBuffer = [_]u8{ 65, 65, 65, 65, 65, 65 }; 388 | var valueBuffer = [_]u8{ 65, 65, 65, 65, 65, 65 }; 389 | _ = c.sprintf(&keyBuffer[0], "%d", i % 1000); 390 | _ = c.sprintf(&valueBuffer[0], "%d", (i + 1) % 1000); 391 | // debug.print(" {} -> {} , key length {}\n", .{ keyBuffer, valueBuffer, keyBuffer.len }); 392 | try l.put(keyBuffer[0..], valueBuffer[0..]); 393 | const opt = try l.get(keyBuffer[0..]); 394 | allocator.destroy(opt.?); 395 | i += 1; 396 | // used for reduce pression in test 397 | if (i % 100_000 == 0) { 398 | _ = c.sleep(2); 399 | } 400 | } 401 | var s = "1\x00AAAA"; 402 | const t = mem.span(s); 403 | debug.print("test key length : {}\n", .{t[0..].len}); 404 | const lecturealea = try l.get(t[0..]); 405 | debug.assert(lecturealea != null); 406 | debug.print("retrieved : {}\n", .{lecturealea}); 407 | if (lecturealea) |value| { 408 | allocator.destroy(value); 409 | } 410 | const it = try l.iterator(); 411 | defer allocator.destroy(it); 412 | defer l.deinit(); 413 | it.first(); 414 | while (it.isValid()) { 415 | const optK = it.iterKey(); 416 | const optV = it.iterValue(); 417 | if (optK) |k| { 418 | defer allocator.destroy(k); 419 | debug.print(" {any} value : {any}\n", .{ k.*, optV.?.* }); 420 | // debug.print(" key for string \"{}\" \n", .{k.*}); 421 | const ovbg = try l.get(k.*); 422 | if (ovbg) |rv| { 423 | debug.print(" {any} value : {any}\n", .{ k.*, rv.* }); 424 | } 425 | } 426 | if (optV) |v| { 427 | defer allocator.destroy(v); 428 | debug.print(" value for string \"{any}\" \n", .{v.*}); 429 | } 430 | it.next(); 431 | } 432 | it.deinit(); 433 | 434 | l.close(); 435 | } 436 | 437 | // test "test marshalling" { 438 | // debug.print("start marshall tests\n", .{}); 439 | 440 | // var allocator = std.heap.c_allocator; 441 | 442 | // const StringMarshall = ArrSerDeserType(u8); 443 | // const stringToMarshall = "hello\x00"; 444 | // debug.print("original string ptr \"{}\"\n", .{@ptrToInt(stringToMarshall)}); 445 | 446 | // const sspan = mem.span(stringToMarshall); 447 | // debug.print("span type \"{}\"\n", .{@typeInfo(@TypeOf(sspan))}); 448 | // const marshalledC = StringMarshall.marshall(&sspan, &allocator); 449 | // debug.print("marshalled \"{any}\", ptr {any} \n", .{ marshalledC, marshalledC.ptr }); 450 | // debug.print("pointer to first element {} \n", .{@ptrToInt(marshalledC.ptr)}); 451 | 452 | // debug.assert(&marshalledC[0] == &stringToMarshall[0]); 453 | // } 454 | 455 | // test "test reading" { 456 | // var filename = "countingstorage\x00"; 457 | 458 | // var allocator = std.heap.c_allocator; 459 | 460 | // const SS = LevelDBHash(u32, u32); 461 | // var l = try SS.init(&allocator); 462 | // defer l.deinit(); 463 | 464 | // _ = try l.open(filename); 465 | // var i: u32 = 0; 466 | // while (i < 1000) { 467 | // const v = try l.get(i); 468 | // debug.assert(v.?.* == i + 10); 469 | // i += 1; 470 | // } 471 | // l.close(); 472 | // } 473 | 474 | // test "test serialization types" { 475 | // // var filename : [100]u8 = [_]u8{0} ** 100; 476 | // var filename = "hellosimpletypes2\x00"; 477 | 478 | // var allocator = std.heap.c_allocator; 479 | 480 | // const u32SerializationType = SerDeserType(u32); 481 | // const u8ArrSerializationType = ArrSerDeserType(u8); 482 | // const SS = LevelDBHashWithSerialization(u32, u32, u32, u8, []const u8, []u8, u32SerializationType, u8ArrSerializationType); 483 | 484 | // var l = try SS.init(&allocator); 485 | 486 | // _ = try l.open(filename); 487 | // var h = @intCast(u32, 5); 488 | // var w = [_]u8{ 'w', 'o', 'r', 'l', 'd', 0x00 }; 489 | // // debug.print("slice size : {}\n", .{w[0..].len}); 490 | // _ = try l.put(h, w[0..]); 491 | 492 | // const t = try l.get(h); 493 | // debug.print("returned value {}\n", .{t.?.*}); 494 | // if (t) |result| { 495 | // debug.print("result is :{}\n", .{result.*}); 496 | // // debug.print("result type is :{}\n", .{@TypeOf(result.*)}); 497 | // } 498 | 499 | // defer l.close(); 500 | // } 501 | 502 | // RAW C API Tests 503 | 504 | //test "creating file" { 505 | // const options = cleveldb.leveldb_options_create(); 506 | // cleveldb.leveldb_options_set_create_if_missing(options, 1); 507 | // var err: [*c]u8 = null; 508 | // // const db = c.leveldb_open(options, "testdb", @intToPtr([*c][*c]u8,@ptrToInt(&err[0..]))); 509 | // const db = cleveldb.leveldb_open(options, "testdb", &err); 510 | // if (err != null) { 511 | // _ = c.printf("open failed"); 512 | // defer cleveldb.leveldb_free(err); 513 | // return; 514 | // } 515 | // var woptions = cleveldb.leveldb_writeoptions_create(); 516 | // cleveldb.leveldb_put(db, woptions, "key", 3, "value", 6, &err); 517 | // 518 | // const roptions = cleveldb.leveldb_readoptions_create(); 519 | // var read: [*c]u8 = null; 520 | // var read_len: usize = 0; 521 | // read = cleveldb.leveldb_get(db, roptions, "key", 3, &read_len, &err); 522 | // if (err != null) { 523 | // _ = c.printf("open failed"); 524 | // defer cleveldb.leveldb_free(err); 525 | // return; 526 | // } 527 | // _ = c.printf("returned : %s %d\n", read, read_len); 528 | // cleveldb.leveldb_close(db); 529 | //} 530 | -------------------------------------------------------------------------------- /man/iotmonitor.1.md: -------------------------------------------------------------------------------- 1 | % IOTMONITOR(1) iotmonitor 0.2.3 2 | % Patrice Freydiere 3 | % December 2021 4 | 5 | # NAME 6 | iotmonitor - monitor and manage execution of mqtt iot processes 7 | 8 | # SYNOPSIS 9 | **iotmonitor** [*OPTIONS*] 10 | 11 | # DESCRIPTION 12 | 13 | **iotMonitor** is an effortless and lightweight mqtt monitoring for devices (things) and agents on Linux. 14 | 15 | IotMonitor aims to solve the "always up" problem of large IOT devices and agents system. This project is successfully used every day for running smart home automation system. 16 | Considering large and longlived running mqtt systems can hardly rely only on monolytics plateforms, the reality is always composite as some agents or functionnalities increase with time. Diversity also occurs in running several programming languages implementation for agents. 17 | 18 | This project offers a simple command line, as in \*nix system, to monitor MQTT device or agents system. MQTT based communication devices (IOT) and agents are watched, and alerts are emitted if devices or agents are not responding any more. Declared software agents are restarted by iotmonitor when crashed. 19 | 20 | # OPTIONS 21 | 22 | **--help** 23 | : Display a friendly help message 24 | 25 | **-v**, **--version** 26 | : Display the software version 27 | 28 | # IOTMONITOR MESSAGE PUBLISHING 29 | 30 | Once the mqtt topics associated to a thing or agent is declared, IotMonitor records and restore given MQTT "states topics" as they go and recover. It helps reinstalling IOT things state, to avoid lots of administration tasks. 31 | 32 | ## "EXPIRED" SUBTOPIC 33 | 34 | IotMonitor use a TOML config file. Each device has an independent configured communication message time out. When the device stop communication on this topic, the iotmonitor publish a specific monitoring failure topic for the lots device, with the latest contact timestamp. This topic is labelled : 35 | 36 | [BASE_IOTMONITOR_TOPIC]/expire/[device_name] 37 | for example : 38 | 39 | home/monitoring/expire/[device_name] 40 | 41 | This topic can then be displayed or alerted to inform that the device or agent is not working properly. 42 | 43 | ## "UP" SUBTOPIC 44 | 45 | When running, iotmonitor also published an *up* subtopic this topic is published every seconds and provide the startup seconds of the iotmonitor process. This topic can be also used to monitor the iotmonitor process. 46 | 47 | ## "HELLOTOPICCOUNT" SUBTOPIC 48 | 49 | helloTopicCount count, for each device, the number of hello topic published by the device or agent. This counter inform the count of restart or reboot of each device or agent. 50 | 51 | [BASE_IOTMONITOR_TOPIC]/helloTopicCount 52 | 53 | 54 | 55 | # CONFIG FILE REFERENCE 56 | 57 | The configuration is defined in the `config.toml` TOML file, (see an example in the root directory) 58 | the global _Mqtt broker configuration section_ is defined using a heading `[mqtt]` 59 | the following parameters are found : 60 | 61 | ```toml 62 | [mqtt] 63 | serverAddress="tcp://localhost:1883" 64 | baseTopic="home/monitoring" 65 | user="" 66 | password="" 67 | ``` 68 | 69 | ## GLOBAL SECTION 70 | 71 | global section of the mqtt configuration file contains the following possible elements : 72 | 73 | serverAddress 74 | : mqtt broker address, this include tcp:// communication and the port as shown in the previous example 75 | 76 | user 77 | : mqtt broker authentication, left empty if no authentication is necessary. The password parameter is then used if the user parameter is filled. 78 | 79 | password 80 | : account associated password 81 | 82 | baseTopic 83 | : root of all iotmonitor publications. This defines the *BASE_IOTMONITOR_TOPIC* 84 | 85 | clientid 86 | : An optional clientid can also be specified to change the mqtt clientid, a default "iotmonitor" value is used when not specified 87 | 88 | ## DEVICE DECLARATION SECTION 89 | 90 | Each monitored device is declared in a section using a "device_" prefix in this section : the following elements can be found : 91 | 92 | ```toml 93 | [device_esp04] 94 | watchTimeOut=60 95 | helloTopic="home/esp04" 96 | watchTopics="home/esp04/sensors/#" 97 | stateTopics="home/esp04/actuators/#" 98 | ``` 99 | 100 | watchTimeOut 101 | : watch dog for alive state, when the timeout is reached without and interactions on watchTopics, then iotmonitor trigger an expire message for the device 102 | 103 | helloTopic 104 | : the topic to observe to welcome the device. This topic trigger the state recovering for the device and agents. IotMonitor, resend the previous stored `stateTopics` 105 | 106 | watchTopics 107 | : the topic pattern to observe to know the device is alive 108 | 109 | stateTopics 110 | : list of topics for recording the states and reset them as they are welcomed 111 | 112 | ## AGENT DECLARATION SECTION 113 | 114 | Agents are declared using an "agent_" prefix in the section. Agents are devices with an associated command line (`exec` config key) that trigger the start of the software agent. IotMonitor checks periodically if the process is running, and relaunch it if needed. 115 | 116 | ```toml 117 | [agent_ledboxdaemon] 118 | exec="source ~/mqttagents/p3/bin/activate;cd ~/mqttagents/mqtt-agent-ledbox;python3 ledboxdaemon.py" 119 | watchTopics="home/agents/ledbox/#" 120 | ``` 121 | 122 | When IotMonitor run the processes, it identify the process by searching a specific command line parameter, containing an IOTMONITOR tag. When executing the agents processes, the processes are detached from the main iotmonitor process, avoiding to relaunch the whole system in the case of restarting the `iotmonitor` process. 123 | 124 | Agents declaration inherit all DEVICE SECTION parameters, with an additional *exec* parameter. 125 | Agents may also have `helloTopic`, `stateTopics` and `watchTimeOut` as previously described in DEVICE DECLARATION SECTION. 126 | 127 | 128 | # STATE RECOVERY 129 | 130 | At startup OR when the `helloTopic` is fired, iotmonitor fire the previousely recorded states on mqtt, this permit the device (things), to take it's previoulsy state, as if it has not been stopped.. All mqtt recorded states (`stateTopics`) are backuped by iotmonitor in a leveldb database. 131 | For practical reasons, this permit to centralize the state, and restore them when an iot device has rebooted. If used this functionnality, reduce the need to implement a cold state storage for each agent or device. Starting or stopping iotmonitor, redefine the state for all elements. 132 | 133 | 134 | -------------------------------------------------------------------------------- /man/sections_to_cover.txt: -------------------------------------------------------------------------------- 1 | 2 | Name: The name of the command and a pithy one-liner that describes its function. 3 | Synopsis: A terse description of the invocations someone can use to launch the program. These show the types of accepted command-line parameters. 4 | Description: A description of the command or function. 5 | Options: A list of command-line options, and what they do. 6 | Examples: Some examples of common usage. 7 | Exit Values: The possible return codes and their meanings. 8 | Bugs: A list of known bugs and quirks. Sometimes, this is supplemented with (or replaced by) a link to the issue tracker for the project. 9 | Author: The person or people who wrote the command. 10 | Copyright: Your copyright message. These also usually include the type of license under which the program is released. 11 | 12 | 13 | copy to /usr/share/man/man1 14 | 15 | -------------------------------------------------------------------------------- /man/viewdoc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | pandoc iotmonitor.1.md -s -t man | /usr/bin/man -l - 5 | 6 | 7 | -------------------------------------------------------------------------------- /mqttlib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const debug = std.debug; 4 | const log = std.log; 5 | 6 | //const cmqtt = @import("mqtt_paho.zig"); 7 | const cmqtt = @cImport({ 8 | @cInclude("MQTTClient.h"); 9 | }); 10 | const c = @cImport({ 11 | @cInclude("stdio.h"); 12 | @cInclude("unistd.h"); 13 | @cInclude("string.h"); 14 | @cInclude("time.h"); 15 | //@cInclude("paho.mqtt.c/src/MQTTClient.h"); 16 | }); 17 | 18 | const CallBack = fn (topic: []u8, message: []u8) void; 19 | 20 | const MAX_REGISTERED_TOPICS = 10; 21 | 22 | const mqttlibError = error{FailToRegister}; 23 | 24 | // Connexion to mqtt 25 | // 26 | pub const MqttCnx = struct { 27 | const Self = @This(); 28 | 29 | allocator: *mem.Allocator, 30 | handle: cmqtt.MQTTClient = undefined, 31 | callBack: CallBack = undefined, 32 | latestDeliveryToken: *cmqtt.MQTTClient_deliveryToken = undefined, 33 | connected: bool = undefined, 34 | connect_option: *cmqtt.MQTTClient_connectOptions = undefined, 35 | 36 | reconnect_registered_topics_length: u16 = 0, 37 | reconnect_registered_topics: [10]?[]u8, 38 | 39 | // this message is received in an other thread 40 | fn _defaultCallback(topic: []u8, message: []u8) void { 41 | _ = c.printf("%s -> %s\n", topic.ptr, message.ptr); 42 | } 43 | 44 | fn _connLost(ctx: ?*anyopaque, m: [*c]u8) callconv(.C) void { 45 | _ = m; 46 | const self_ctx = @intToPtr(*Self, @ptrToInt(ctx)); 47 | _ = c.printf("connection lost %d\n", self_ctx); 48 | } 49 | 50 | fn _msgArrived(ctx: ?*anyopaque, topic: [*c]u8, topic_length: c_int, message: [*c]cmqtt.MQTTClient_message) callconv(.C) c_int { 51 | _ = topic_length; 52 | 53 | const messagePtrAddress = @ptrToInt(&message); 54 | var cmsg = @intToPtr([*c][*c]cmqtt.MQTTClient_message, messagePtrAddress); 55 | defer cmqtt.MQTTClient_freeMessage(cmsg); 56 | defer cmqtt.MQTTClient_free(topic); 57 | 58 | // unsafe conversion 59 | const self_ctx = @intToPtr(*Self, @ptrToInt(ctx)); 60 | 61 | // paho always return a 0 in topic_length, we must then compute it 62 | // _ = c.printf("topic length is %d \n", topic_length); 63 | 64 | const tlength: usize = c.strlen(topic); 65 | // topic_length always 0 ... non sense 66 | const mlength = @intCast(u32, message.*.payloadlen); 67 | 68 | const am = @ptrToInt(message.*.payload); 69 | const mptr = @intToPtr([*]u8, am); 70 | // pass to zig in proper way with slices 71 | self_ctx.callBack(topic[0..tlength], mptr[0..mlength]); 72 | 73 | return 1; // properly handled 74 | } 75 | 76 | fn _delivered(ctx: ?*anyopaque, token: cmqtt.MQTTClient_deliveryToken) callconv(.C) void { 77 | 78 | // _ = c.printf("%s", "received token"); 79 | // unsafe conversion 80 | const self_ctx = @intToPtr(*Self, @ptrToInt(ctx)); 81 | self_ctx.*.latestDeliveryToken.* = token; 82 | } 83 | 84 | pub fn deinit(self: *Self) !void { 85 | _ = self; 86 | } 87 | 88 | pub fn init(allocator: *mem.Allocator, serverAddress: []const u8, clientid: []const u8, username: []const u8, password: []const u8) !*Self { 89 | var handle: cmqtt.MQTTClient = undefined; 90 | 91 | // we need to make a safe string with zero ending 92 | const zServerAddress = try allocator.alloc(u8, serverAddress.len + 1); 93 | // defer allocator.free(zServerAddress); 94 | mem.copy(u8, zServerAddress, serverAddress[0..]); 95 | zServerAddress[serverAddress.len] = '\x00'; 96 | 97 | const zusername = try allocator.alloc(u8, username.len + 1); 98 | // defer allocator.free(zusername); 99 | mem.copy(u8, zusername, username[0..]); 100 | zusername[username.len] = '\x00'; 101 | 102 | const zpassword = try allocator.alloc(u8, password.len + 1); 103 | // defer allocator.free(zpassword); 104 | mem.copy(u8, zpassword, password[0..]); 105 | zpassword[password.len] = '\x00'; 106 | 107 | const zclientid = try allocator.alloc(u8, clientid.len + 1); 108 | // defer allocator.free(zclientid); 109 | mem.copy(u8, zclientid, clientid[0..]); 110 | zclientid[clientid.len] = '\x00'; 111 | 112 | // convert to C the input parameters (ensuring the sentinel) 113 | const MQTTCLIENT_PERSISTENCE_NONE = 1; 114 | const result = cmqtt.MQTTClient_create(&handle, zServerAddress.ptr, zclientid.ptr, MQTTCLIENT_PERSISTENCE_NONE, null); 115 | 116 | if (result > 0) return error.MQTTCreateError; 117 | const HEADER = [_]u8{ 'M', 'Q', 'T', 'C' }; 118 | 119 | // we setup the struct here, because the initializer is a macro in C, 120 | var conn_options = cmqtt.MQTTClient_connectOptions{ 121 | .struct_id = HEADER, 122 | .struct_version = 0, 123 | .keepAliveInterval = 60, 124 | .cleansession = 1, 125 | .reliable = 1, 126 | .will = null, 127 | .username = zusername.ptr, 128 | .password = zpassword.ptr, 129 | .connectTimeout = 30, 130 | .retryInterval = 0, 131 | .ssl = null, 132 | .serverURIcount = 0, 133 | .serverURIs = null, 134 | .MQTTVersion = 0, 135 | .returned = .{ 136 | .serverURI = null, 137 | .MQTTVersion = 0, 138 | .sessionPresent = 0, 139 | }, 140 | .binarypwd = .{ 141 | .len = 0, 142 | .data = null, 143 | }, 144 | .maxInflightMessages = -1, 145 | .cleanstart = 0, // only available on V5 + 146 | .httpHeaders = null, 147 | }; 148 | 149 | if (username.len == 0) { 150 | conn_options.username = null; 151 | conn_options.password = null; 152 | } 153 | 154 | var self_ptr = try allocator.create(Self); 155 | // init members 156 | self_ptr.handle = handle; 157 | self_ptr.allocator = allocator; 158 | self_ptr.callBack = _defaultCallback; 159 | self_ptr.latestDeliveryToken = try allocator.create(cmqtt.MQTTClient_deliveryToken); 160 | self_ptr.connected = false; 161 | // remember the connect options 162 | self_ptr.connect_option = try allocator.create(cmqtt.MQTTClient_connectOptions); 163 | self_ptr.connect_option.* = conn_options; 164 | self_ptr.reconnect_registered_topics = mem.zeroes([10]?[]u8); 165 | self_ptr.reconnect_registered_topics_length = 0; 166 | 167 | const retCallBacks = cmqtt.MQTTClient_setCallbacks(handle, self_ptr, _connLost, _msgArrived, _delivered); 168 | if (retCallBacks != cmqtt.MQTTCLIENT_SUCCESS) { 169 | return mqttlibError.FailToRegister; 170 | } 171 | try self_ptr.reconnect(true); 172 | return self_ptr; 173 | } 174 | 175 | fn reconnect(self: *Self, first: bool) !void { 176 | if (self.*.connected) { 177 | // nothing to do 178 | return; 179 | } 180 | 181 | if (!first) { 182 | const result = cmqtt.MQTTClient_disconnect(self.handle, 100); 183 | if (result != 0) { 184 | _ = c.printf("disconnection failed MQTTClient_disconnect returned %d, continue\n", result); 185 | } 186 | } 187 | 188 | const r = cmqtt.MQTTClient_connect(self.handle, self.connect_option); 189 | _ = c.printf("connect to mqtt returned %d\n", r); 190 | if (r != 0) return error.MQTTConnectError; 191 | self.connected = true; 192 | 193 | if (self.reconnect_registered_topics_length > 0) { 194 | for (self.reconnect_registered_topics[0..self.reconnect_registered_topics_length]) |e| { 195 | if (e) |nonNullPtr| { 196 | _ = c.printf("re-registering %s \n", nonNullPtr.ptr); 197 | self._register(nonNullPtr) catch { 198 | _ = c.printf("cannot reregister \n"); 199 | }; 200 | } 201 | } 202 | } 203 | } 204 | 205 | // publish a message with default QOS 0 206 | pub fn publish(self: *Self, topic: [*c]const u8, msg: []const u8) !void { 207 | return publishWithQos(self, topic, msg, 0); 208 | } 209 | 210 | pub fn publishWithQos(self: *Self, topic: [*c]const u8, msg: []const u8, qos: u8) !void { 211 | self._publishWithQos(topic, msg, qos) catch |e| { 212 | _ = c.printf("fail to publish, try to reconnect \n"); 213 | log.warn("error : {}", .{e}); 214 | self.connected = false; 215 | self.reconnect(false) catch |reconnecterr| { 216 | _ = c.printf("failed to reconnect, will retry later \n"); 217 | log.warn("error: {}", .{reconnecterr}); 218 | }; 219 | }; 220 | } 221 | 222 | // internal method, to permit to retry connect 223 | fn _publishWithQos(self: *Self, topic: [*c]const u8, msg: []const u8, qos: u8) !void { 224 | const messageLength: c_int = @intCast(c_int, msg.len); 225 | 226 | if (msg.len == 0) { 227 | return; 228 | } 229 | // beacause c declared the message as mutable (not const), 230 | // convert it to const type 231 | const constMessageContent: [*]u8 = @intToPtr([*]u8, @ptrToInt(msg.ptr)); 232 | 233 | const HEADER_MESSAGE = [_]u8{ 'M', 'Q', 'T', 'M' }; 234 | 235 | var mqttmessage = cmqtt.MQTTClient_message{ 236 | .struct_id = HEADER_MESSAGE, 237 | .struct_version = 0, // no message properties 238 | .payloadlen = messageLength, 239 | .payload = constMessageContent, 240 | .qos = qos, 241 | .retained = 0, 242 | .dup = 0, 243 | .msgid = 0, 244 | // below, these are MQTTV5 specific properties 245 | .properties = cmqtt.MQTTProperties{ 246 | .count = 0, 247 | .max_count = 0, 248 | .length = 0, 249 | .array = null, 250 | }, 251 | }; 252 | 253 | var token = try self.allocator.create(cmqtt.MQTTClient_deliveryToken); 254 | defer self.allocator.destroy(token); 255 | 256 | const resultPublish = cmqtt.MQTTClient_publishMessage(self.handle, topic, &mqttmessage, token); 257 | if (resultPublish != 0) { 258 | std.log.warn("publish mqtt message returned {}\n", .{resultPublish}); 259 | return error.MQTTPublishError; 260 | } 261 | 262 | // wait for sent 263 | 264 | if (qos > 0) { 265 | const waitResult = cmqtt.MQTTClient_waitForCompletion(self.handle, token.*, @intCast(c_ulong, 2000)); 266 | if (waitResult != 0) return error.MQTTWaitTokenError; 267 | while (self.latestDeliveryToken.* != token.*) { 268 | // CPU breath, and yield 269 | _ = c.usleep(1); 270 | } 271 | } 272 | } 273 | 274 | pub fn register(self: *Self, topic: []const u8) !void { 275 | // remember the topic, to be able to re register at connection lost 276 | // 277 | if (self.*.reconnect_registered_topics_length >= self.reconnect_registered_topics.len) { 278 | // not enought room remember registered topics 279 | return error.TooMuchRegisteredTopics; 280 | } 281 | 282 | const ptr = try self.allocator.alloc(u8, topic.len + 1); 283 | mem.copy(u8, ptr, topic[0..]); 284 | ptr[topic.len] = '\x00'; 285 | self.reconnect_registered_topics[self.*.reconnect_registered_topics_length] = ptr; 286 | self.reconnect_registered_topics_length = self.*.reconnect_registered_topics_length + 1; 287 | try self._register(ptr); 288 | } 289 | 290 | fn _register(self: *Self, topic: []const u8) !void { 291 | _ = c.printf("register to %s \n", topic.ptr); 292 | const r = cmqtt.MQTTClient_subscribe(self.*.handle, topic.ptr, 0); 293 | if (r != 0) return error.MQTTRegistrationError; 294 | } 295 | }; 296 | 297 | test "testconnect mqtt home" { 298 | var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); 299 | defer arena.deinit(); 300 | const allocator = &arena.allocator; 301 | 302 | var serverAddress: []const u8 = "tcp://192.168.4.16:1883"; 303 | var clientid: []const u8 = "clientid"; 304 | var userName: []const u8 = "sys"; 305 | var password: []const u8 = "pwd"; 306 | 307 | // var handle: cmqtt.MQTTClient = null; 308 | 309 | var cnx = try MqttCnx.init(allocator, serverAddress, clientid, userName, password); 310 | 311 | const myStaticMessage: []const u8 = "Hello static message"; 312 | 313 | _ = try cnx.register("home/#"); 314 | 315 | _ = c.sleep(10); 316 | 317 | var i: u32 = 0; 318 | while (i < 10000) : (i += 1) { 319 | try cnx.publish("myothertopic", myStaticMessage); 320 | } 321 | 322 | while (i < 10000) : (i += 1) { 323 | try cnx.publishWithQos("myothertopic", myStaticMessage, 1); 324 | } 325 | _ = c.printf("ended"); 326 | } 327 | 328 | pub fn main() !void { 329 | var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); 330 | defer arena.deinit(); 331 | const allocator = &arena.allocator; 332 | 333 | var serverAddress: []const u8 = "tcp://192.168.4.16:1883"; 334 | var clientid: []const u8 = "clientid"; 335 | var userName: []const u8 = "sys"; 336 | var password: []const u8 = "pwd"; 337 | 338 | var cnx = try MqttCnx.init(allocator, serverAddress, clientid, userName, password); 339 | _ = try cnx.register("home/#"); 340 | _ = c.sleep(20); 341 | _ = c.printf("ended"); 342 | 343 | return; 344 | } 345 | -------------------------------------------------------------------------------- /nozig-tracy/src/lib.zig: -------------------------------------------------------------------------------- 1 | 2 | // This library mock the tracy calls 3 | 4 | const std = @import("std"); 5 | 6 | pub const Ctx = struct { 7 | 8 | pub fn end(self: Ctx) void { 9 | // no op 10 | _ = self; 11 | } 12 | }; 13 | 14 | 15 | pub fn trace(comptime src: std.builtin.SourceLocation) callconv(.Inline) Ctx { 16 | _ = src; 17 | return Ctx{ 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /processlib.zig: -------------------------------------------------------------------------------- 1 | // process lib, permit to launch loosely coupled processes 2 | 3 | const std = @import("std"); 4 | const debug = std.debug; 5 | const log = std.log; 6 | const os = std.os; 7 | const mem = std.mem; 8 | const fs = std.fs; 9 | const File = fs.File; 10 | const Dir = fs.Dir; 11 | const fmt = std.fmt; 12 | 13 | pub const ProcessInformation = struct { 14 | pid: i32 = 0, 15 | // 8k for buffer ? is it enought ? 16 | commandlinebuffer: [BUFFERSIZE]u8 = [_]u8{'\x00'} ** BUFFERSIZE, 17 | commandlinebuffer_size: u16 = 0, 18 | const Self = @This(); 19 | 20 | const BUFFERSIZE = 8292 * 2; 21 | 22 | // iterator on the command line arguments 23 | const Iterator = struct { 24 | inner: *const Self, 25 | currentIndex: u16 = 0, 26 | 27 | const SelfIterator = @This(); 28 | pub fn next(self: *SelfIterator) ?[]const u8 { 29 | if (self.currentIndex >= self.inner.commandlinebuffer_size) { 30 | return null; 31 | } 32 | const start = self.currentIndex; 33 | // seek for next 0 ending or end of commandline buffer_size 34 | while (self.currentIndex < self.inner.commandlinebuffer_size and self.inner.commandlinebuffer[self.currentIndex] != '\x00') { 35 | self.currentIndex = self.currentIndex + 1; 36 | } 37 | // move to next after returning the element slice 38 | defer self.currentIndex = self.currentIndex + 1; 39 | return self.inner.commandlinebuffer[start..self.currentIndex]; 40 | } 41 | }; 42 | pub fn iterator(processInfo: *Self) Iterator { 43 | return Iterator{ .inner = processInfo }; 44 | } 45 | }; 46 | 47 | // get process information, for a specific PID 48 | // return false if the process does not exists, 49 | // return true and the processInfo is populated with the command line options 50 | pub fn getProcessInformations(pid: i32, processInfo: *ProcessInformation) !bool { 51 | var buffer = [_]u8{'\x00'} ** 8192; 52 | 53 | const options = Dir.OpenDirOptions{ 54 | .access_sub_paths = true, 55 | .iterate = true, 56 | }; 57 | var procDir = try fs.cwd().openDir("/proc", options); 58 | defer procDir.close(); 59 | 60 | const r = try fmt.bufPrint(&buffer, "{}", .{pid}); 61 | var subprocDir: Dir = procDir.openDir(r, options) catch { 62 | log.warn("no dir associated to proc {}", .{pid}); 63 | return false; 64 | }; 65 | defer subprocDir.close(); 66 | 67 | var commandLineFile: File = try subprocDir.openFile("cmdline", File.OpenFlags{}); 68 | defer commandLineFile.close(); 69 | 70 | const readSize = try commandLineFile.pread(processInfo.commandlinebuffer[0..], 0); 71 | processInfo.commandlinebuffer_size = @intCast(u16, readSize); 72 | 73 | processInfo.*.pid = pid; 74 | 75 | return true; 76 | } 77 | 78 | // test if buffer contains only digits 79 | fn isAllNumeric(buffer: []const u8) bool { 80 | for (buffer) |c| { 81 | if (c > '9' or c < '0') { 82 | return false; 83 | } 84 | } 85 | return true; 86 | } 87 | // Process browsing 88 | // 89 | const ProcessInformationCallback = fn (processInformation: *ProcessInformation) void; 90 | 91 | // function that list processes and grab command line arguments 92 | // a call back is taken from 93 | pub fn listProcesses(callback: ProcessInformationCallback) !void { 94 | const options = Dir.OpenDirOptions{ 95 | .access_sub_paths = true, 96 | .iterate = true, 97 | }; 98 | var procDir = try fs.cwd().openDir("/proc", options); 99 | defer procDir.close(); 100 | 101 | var dirIterator = procDir.iterate(); 102 | while (try dirIterator.next()) |f| { 103 | if (f.kind == File.Kind.File) { 104 | continue; 105 | } 106 | 107 | if (!isAllNumeric(f.name)) { 108 | continue; 109 | } 110 | 111 | const pid = try fmt.parseInt(i32, f.name, 10); 112 | var pi = ProcessInformation{}; 113 | const successGetInformations = getProcessInformations(pid, &pi) catch { 114 | // if file retrieve failed because of pid close, continue 115 | continue; 116 | }; 117 | if (successGetInformations and pi.commandlinebuffer_size > 0) { 118 | callback(&pi); 119 | // log.warn(" {}: {} \n", .{ pid, pi.commandlinebuffer[0..pi.commandlinebuffer_size] }); 120 | } 121 | // try opening the commandline file 122 | } 123 | } 124 | 125 | fn testCallback(processInformation: *ProcessInformation) void { 126 | log.warn("processinformation : {}", .{processInformation}); 127 | // dump the commnand line buffer 128 | var it = processInformation.iterator(); 129 | while (it.next()) |i| { 130 | log.warn(" {}\n", .{i}); 131 | } 132 | } 133 | 134 | test "check existing process" { 135 | try listProcesses(testCallback); 136 | } 137 | -------------------------------------------------------------------------------- /snapcraft/README.md: -------------------------------------------------------------------------------- 1 | 2 | Building snap on command line 3 | 4 | cd snap 5 | 6 | snapcraft 7 | 8 | 9 | cleaning the latest build : 10 | 11 | snapcraft clean 12 | 13 | 14 | 15 | Install on dev stage 16 | 17 | sudo snap install iotmonitor_0.2+git_amd64.snap --devmode --dangerous 18 | 19 | Upload the edge : 20 | 21 | snapcraft upload --release=edge iotmonitor*.snap 22 | 23 | promote the snap to beta or candidate 24 | snapcraft release iotmonitor 5 beta 25 | -------------------------------------------------------------------------------- /snapcraft/snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: iotmonitor 2 | base: core18 3 | version: '0.2.6+git' 4 | summary: Monitor MQTT IOT devices or agents to make them run 5 | description: | 6 | IotMonitor solve the "always up" problem of large IOT devices and agents system. 7 | Considering large and longlived systems cannot rely only on some specific software, 8 | using raw system's processes permit to compose a system with several processes or devices, 9 | implemented with different langages and coming from multiple implementers or third party. 10 | This project is simple command line, as in *nix system, to monitor MQTT device or 11 | agents system. MQTT based communication devices (IOT) and agents are monitored, 12 | and alerts are emitted if devices or agents are not responding. 13 | Software agents are also restarted when crashed. 14 | IotMonitor records and restore MQTT states topics as they go and recover. 15 | It helps to maintain IOT things working, and avoid lots of administration tasks. 16 | 17 | grade: devel 18 | confinement: devmode 19 | 20 | parts: 21 | iotmonitor: 22 | plugin: make 23 | source-type: git 24 | source: https://github.com/frett27/iotmonitor.git 25 | source-branch: zig_0.9.1 26 | build-snaps: [zig/latest/beta] 27 | build-packages: [build-essential, make, cmake, libleveldb-dev] 28 | stage-packages: [libleveldb-dev] 29 | 30 | apps: 31 | iotmonitor: 32 | command: iotmonitor 33 | 34 | 35 | -------------------------------------------------------------------------------- /topics.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | 4 | const assert = std.debug.assert; 5 | 6 | // test if the evaluted topic belong to the reference Topic, 7 | // return the sub path if it is 8 | pub fn doesTopicBelongTo(evaluatedTopic: []const u8, referenceTopic: []const u8) !?[]const u8 { 9 | if (evaluatedTopic.len < referenceTopic.len) return null; 10 | const isWatched = mem.eql(u8, evaluatedTopic[0..referenceTopic.len], referenceTopic); 11 | if (isWatched) { 12 | const subTopic = evaluatedTopic[referenceTopic.len..]; 13 | return subTopic; 14 | } 15 | return null; 16 | } 17 | 18 | test "Test belong to" { 19 | const topic1: []const u8 = "/home"; 20 | const topic2: []const u8 = "/home"; 21 | const a = try doesTopicBelongTo(topic1, topic2); 22 | assert(a != null); 23 | assert(a.?.len == 0); 24 | } 25 | test "sub path" { 26 | const topic1: []const u8 = "/home"; 27 | const topic2: []const u8 = "/ho"; 28 | const a = try doesTopicBelongTo(topic1, topic2); 29 | assert(a != null); 30 | assert(a.?.len == 2); 31 | assert(mem.eql(u8, a.?, "me")); 32 | } 33 | test "no match 1" { 34 | const topic1: []const u8 = "/home"; 35 | const topic2: []const u8 = "/ho2"; 36 | const a = try doesTopicBelongTo(topic1, topic2); 37 | assert(a == null); 38 | } 39 | test "no match 2" { 40 | const topic1: []const u8 = "/home"; 41 | const topic2: []const u8 = "/home2"; 42 | const a = try doesTopicBelongTo(topic1, topic2); 43 | assert(a == null); 44 | } 45 | -------------------------------------------------------------------------------- /version.zig: -------------------------------------------------------------------------------- 1 | pub const version = "0.2.7"; 2 | --------------------------------------------------------------------------------