Files
small_tel/Daemons/weather_logger/server.c

414 lines
13 KiB
C

/*
* This file is part of the meteologger project.
* Copyright 2026 Edward V. Emelianov <edward.emelianoff@gmail.com>.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <dirent.h>
#include <fcntl.h>
#include <regex.h>
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "server.h"
// some "standard" keys from server
static const char *key_pluginname = "PLUGIN";
static const char *key_nvalues = "NVALUES";
// sensor's db filename mask: DBpath/weatherXX.log
static const char *dbfilename_mask = "%s/weather%02d.log";
// and search regex
static const char *dbfilename_regex = "weather[[:digit:]]{2}\\.log";
static volatile atomic_bool isrunning = true;
static volatile atomic_bool logreinit = false;
static double req_interval = 0.5; // request interval, s
static double net_timeout = 1.; // timeout for server's answer, s
static char *DBpath = NULL; // path to storage
typedef struct{
int fd; // log file descriptor
int idx; // index of station for requests
int nvalues; // maximal amount of values
char path[PATH_MAX];// path to file
char *sensname; // sensor's name
char **keys; // keyword names for header and index in string
int *levels; // array of `weather level` for each keyword
} senslog_t;
static int Nsensors = 0; // amount of sensors for logging
static senslog_t *sensors = NULL; // array of files for logging
// commands for communication with server
typedef enum{
CMD_LIST,
CMD_GET,
CMD_CHKLEVEL,
CMD_TIME,
CMD_AMOUNT
} req_commands;
static const char *commands[CMD_AMOUNT] = {
[CMD_LIST] = "list",
[CMD_GET] = "get",
[CMD_CHKLEVEL] = "chklevel",
[CMD_TIME] = "time",
};
static void delete_senskeys(senslog_t *sensor){
if(!sensor) return;
for(int j = 0; j < sensor->nvalues; ++j)
FREE(sensor->keys[j]);
FREE(sensor->keys);
sensor->nvalues = 0;
}
static void sensors_delete(){
if(!sensors) return;
for(int i = 0; i < Nsensors; ++i){
senslog_t *sensor = &sensors[i];
delete_senskeys(sensor);
FREE(sensor->levels);
FREE(sensor->sensname);
}
FREE(sensors);
}
// stop server process
void stop_server(){
FNAME();
isrunning = false;
}
// set request interval to this value
void set_reqinterval(double dt){
req_interval = dt; // all checking is in `parseargs.c`
}
// set network timeout
void set_nettimeout(double dt){
net_timeout = dt;
}
/**
* @brief send_request - send request to server
* @param cmd - command index
* @return true if OK, false if disconnected
*/
static bool send_request(sl_sock_t *sock, const char *req){
if(sl_sock_sendstrmessage(sock, req) < 1){
LOGERR("Can't send request '%s'", req);
return false;
}
return true;
}
// reinit logs at start or after rotating
// @return false if failed
bool reinit_logs(){
FNAME();
logreinit = true;
return true;
}
bool checkDBpath(const char *path){
struct stat path_stat;
if(stat(path, &path_stat)){
WARNX("Can't stat() %s", path);
return false; // `stat` failed
}
if(!S_ISDIR(path_stat.st_mode)){
WARNX("%s isn't a directory", path);
return false; // not a directory
}
// now check if we can write there
if(access(path, W_OK)){
WARNX("Can't write to %s", path);
return false;
}
return true;
}
// find if this sensor already have opened BD;
// if have, modify sensor->fd and sensor->path, open file for appending and return `true`
static bool find_old_file(senslog_t *sensor){
if(sensor->fd > -1){
DBG("found opened file %s with fd %d -> close", sensor->path, sensor->fd);
close(sensor->fd);
return false; // we need to open new file after logrotating
}
regex_t regex;
// Compile regex
if(regcomp(&regex, dbfilename_regex, REG_EXTENDED | REG_NOSUB) != 0){
LOGERR("find_old_file(): error in regcomp(), %s", strerror(errno));
WARNX("regcomp()");
return false;
}
bool ret = false;
DIR *d = opendir(DBpath);
if(d){
struct dirent *dir;
while((dir = readdir(d))){
// Check if filename matches regex
if(regexec(&regex, dir->d_name, 0, NULL, 0) == 0){
DBG("Found: %s", dir->d_name);
char fname[PATH_MAX], line[BUFSIZ];
snprintf(fname, PATH_MAX, "%s/%s", DBpath, dir->d_name);
FILE *fp = fopen(fname, "r");
if(!fp){
LOGERR("Cannot open %s for reading: %s", fname, strerror(errno));
DBG("Can't open");
continue;
}
if(fgets(line, sizeof(line), fp) == NULL){
LOGWARN("Found empty BD file %s - WTF???", fname);
fclose(fp);
continue;
}
fclose(fp);
int len = strlen(line);
if(len > 0 && line[len-1] == '\n') line[len-1] = 0; // remove trailing newline
if(line[0] != '#' || line[1] != ' '){ // should starts from comment with station's name
LOGWARN("Found broken database file: %s", fname);
continue;
}
const char *stname = line + 2; // station name from comment
if(0 == strcmp(stname, sensor->sensname)){ // good, we found this file!
DBG("Found existant file %s -> append to it", fname);
int newfd = open(fname, O_WRONLY | O_APPEND);
if(newfd < 0){
LOGERR("Can't open existant BD file %s for append, try to create new", fname);
continue;
}
sensor->fd = newfd;
ret = true;
break;
}
}
}
closedir(d);
}else{
LOGERR("Can't open %s: %s", DBpath, strerror(errno));
}
regfree(&regex);
return ret;
}
// create new database file in `DBpath`
static bool create_db_file(senslog_t *sensor){
FNAME();
int num = 0;
char path[PATH_MAX];
for(; num <= 99; ++num){
snprintf(path, PATH_MAX, dbfilename_mask, DBpath, num);
DBG("Try to create %s", path);
if(access(path, F_OK) != 0) break; // no such file
}
if(num > 99){
LOGERR("Can't find free filename for station '%s', all numbers from 0 to 99 are busy! WTF???",
sensor->sensname);
WARNX("No free numbers for sensors");
return false;
}
// create and open write-only
int newfd = open(path, O_WRONLY | O_CREAT, 0644);
if(newfd < 0){
LOGERR("Can't open file %s for station %s: %s", path, sensor->sensname, strerror(errno));
WARNX("Can't open %s", path);
return false;
}
DBG("OK, %s opened, try to write header", path);
int len = snprintf(path, PATH_MAX, "# %s\n", sensor->sensname);
if(write(newfd, path, len) != len){
LOGERR("Can't write sensor's name '%s' to file %s: %s", sensor->sensname, path, strerror(errno));
WARNX("Can't write header");
close(newfd);
return false;
}
DBG("%s now have descriptor %d: %s", sensor->sensname, newfd, path);
sensor->fd = newfd;
return true;
}
static bool get_sensor_keys(senslog_t _U_ *sensor, sl_sock_t _U_ *sock){
return true;
}
// prepare BD files and fill `sensors[i].fd` fields; make header in new file or move write pointer to the end of existant
// `sensors` should be prepared already
// this function called at start and on any logs reinit
static bool prepare_files(sl_sock_t *sock){
if(!sock || !DBpath || Nsensors < 1) return false;
for(int i = 0; i < Nsensors; ++i){
senslog_t *sensor = &sensors[i];
if(!get_sensor_keys(sensor, sock)) return false;
DBG("Check if there's something for sensor[%d]", i);
if(!find_old_file(sensor)){ // create new file
DBG("Nothing found, try to create new");
if(!create_db_file(sensor)){
DBG("Oops: can't create");
return false;
}
}
}
DBG("All ready!");
return true;
}
static bool analyse_list(sl_sock_t *sock){
FNAME();
char str[BUFSIZ], key[SL_KEY_LEN], value[SL_VAL_LEN];
double tlast = sl_dtime();
while(sl_dtime() - tlast < net_timeout){
ssize_t len = sl_sock_readline(sock, str, BUFSIZ-1);
if(len == 0) continue;
if(len < 0){
WARNX("Seems like server disconnected");
LOGWARN("Server disconnected?");
sensors_delete();
return false;
}
tlast = sl_dtime();
DBG("Got answer: %s", str);
if(2 != sl_get_keyval(str, key, value)){
LOGWARN("Wrong answer from meteodaemon for 'list' request: %s", str);
continue;
}
DBG("key: '%s', value: '%s'", key, value);
int idx;
// we don't need `str` now and can use it again
if(2 != sscanf(key, "%[^[][%d]", str, &idx)){
LOGWARN("Wrong key format: '%s'", key);
continue;
}
DBG("Got key '%s' with idx=%d", str, idx);
if(Nsensors <= idx){
int oldN = Nsensors;
Nsensors = idx + 1;
sensors = realloc(sensors, Nsensors * sizeof(senslog_t));
if(!sensors){
sensors_delete();
LOGERR("analyse_list(): error in realloc()");
WARNX("analyse_list(): error in realloc()");
return false;
}
bzero(&sensors[oldN], sizeof(senslog_t)*(Nsensors - oldN));
}
senslog_t *sensor = &sensors[idx];
if(0 == strcmp(str, key_pluginname)){ // found plugin name -> fill this field
if(sensor->sensname) free(sensor->sensname);
sensor->sensname = strdup(value);
sensor->idx = idx;
sensor->fd = -1; // not inited yet
DBG("name[%d]=%s", idx, sensor->sensname);
}else if(0 == strcmp(str, key_nvalues)){
int nvalues = atoi(value);
if(nvalues < 1){
LOGWARN("wrong server's responce for sensor[%d] values amount: %d", idx, nvalues);
continue;
}
if(sensor->nvalues) delete_senskeys(sensor);
sensor->nvalues = nvalues;
DBG("sensor[%d] have %d values", idx, sensor->nvalues);
sensor->keys = MALLOC(char*, nvalues); // prepare empty array for keywords
sensor->levels = MALLOC(int, nvalues);
}
}
// now check all we got
if(Nsensors < 1){
LOGWARN("Found 0 sensors in server's answer");
WARNX("Found 0 sensors in server's answer");
return false;
}
bool ans = true;
for(int i = 0; i < Nsensors; ++i){
senslog_t *sensor = &sensors[i];
DBG("Check sensor %s with values %d", sensor->sensname, sensor->nvalues);
if(sensor->nvalues < 1){
LOGWARN("Zero keys for station %s", sensor->sensname);
ans = false; break;
}
}
if(ans) ans = prepare_files(sock);
if(ans == false) sensors_delete();
return ans;
}
/*for(int i = 0; i < nvalues; ++i){
sensor->keys[i] = ;
}*/
// prepare DB files at start
static bool prepare_logfiles(sl_sock_t *sock, const char *path){
FNAME();
char buf[PATH_MAX];
if(DBpath) return false; // already inited??
if(!checkDBpath(path)) return false;
DBpath = strdup(path);
if(!DBpath){
WARN("strdup()");
return false;
}
DBG("Store files in %s; send `list` request", DBpath);
snprintf(buf, 255, "%s\n", commands[CMD_LIST]);
if(!send_request(sock, buf)){
WARNX("Can't send inited request");
return false;
}
// now we have an answer: 2*N strings a la "PLUGIN[i]=...\nNVALUES[i]=...\n" ->
// prepare `sensors` and try to find opened files like `sensorXX.log`
if(!analyse_list(sock)) return false;
return true;
}
/**
* @brief run_server - run main server: send weather requests and store data
* @param node - node to connect
* @param type - socket type
* @param path - directory where to store data logs
*/
void run_server(const char *node, sl_socktype_e type, const char *path){
if(!node || !path) return;
sl_sock_t *sock = sl_sock_run_client(type, node, BUFSIZ);
if(!sock){
DBG("Can't connect");
LOGERR("Can't connect to %s", node);
return;
}
if(!prepare_logfiles(sock, path)) return;
DBG("Superloop");
int errctr = 0;
while(isrunning){
if(logreinit){
if(!prepare_files(sock)) ++errctr;
else logreinit = false;
}
if(errctr > 5){
LOGERR("Too much errors -> exit");
break;
}
usleep(1000);
}
if(sock) sl_sock_delete(&sock);
sensors_delete();
}