Переглянути джерело

Server: Implementierung SQLiteDB, Timer + ServerConnector

Christian Kahlau 3 роки тому
батько
коміт
d757342e13

+ 2 - 1
.gitignore

@@ -5,4 +5,5 @@ daemon/data/
 daemon/dist/
 
 server/public/
-server/dist/
+server/dist/
+server/data/

+ 12 - 0
common/types/server.d.ts

@@ -0,0 +1,12 @@
+type Server = {
+  id: number;
+  title: string;
+  fqdn: string;
+
+  config: { [key: string]: string };
+  data?: {
+    [timestamp: number]: {
+      [key: string]: number;
+    };
+  };
+};

+ 2 - 1
server/.env

@@ -1,2 +1,3 @@
 LOG_LEVEL=INFO
-WEB_PORT=8880
+WEB_PORT=8880
+DATA_DIR=data

+ 3 - 1
server/package.json

@@ -11,11 +11,13 @@
   "license": "ISC",
   "dependencies": {
     "dotenv": "^16.0.2",
-    "express": "^4.18.1"
+    "express": "^4.18.1",
+    "sqlite3": "^5.1.1"
   },
   "devDependencies": {
     "@types/express": "^4.17.14",
     "@types/node": "^18.7.19",
+    "@types/sqlite3": "^3.1.8",
     "typescript": "^4.8.3"
   }
 }

+ 131 - 0
server/src/database.class.ts

@@ -0,0 +1,131 @@
+import fs from 'fs';
+import fsp from 'fs/promises';
+import path from 'path';
+import { Database as SQLiteDB, OPEN_CREATE, OPEN_READWRITE, RunResult } from 'sqlite3';
+
+import { Logger } from '../../common/util/logger.class';
+
+export class Database {
+  private db: SQLiteDB;
+
+  public async open() {
+    try {
+      const DATA_DIR = process.env.DATA_DIR || 'data';
+
+      if (!fs.existsSync(DATA_DIR)) await fsp.mkdir(DATA_DIR);
+
+      const DATA_FILE = path.resolve(DATA_DIR, 'data.db');
+      const exists = fs.existsSync(DATA_FILE);
+
+      await new Promise<void>((res, rej) => {
+        this.db = new SQLiteDB(DATA_FILE, OPEN_READWRITE | OPEN_CREATE, err => (err ? rej(err) : res()));
+      });
+      Logger.info('[INFO]', exists ? 'Opened' : 'Created', 'SQLite3 Database file', DATA_FILE);
+
+      if (!exists) {
+        // INITIAL TABLE SETUP
+        await this.run(
+          `CREATE TABLE Server (
+            ID INTEGER PRIMARY KEY AUTOINCREMENT,
+            Title TEXT NOT NULL UNIQUE,
+            FQDN TEXT NOT NULL UNIQUE
+           );`,
+          []
+        );
+
+        await this.run(
+          `CREATE TABLE ServerConfig (
+            ID INTEGER PRIMARY KEY AUTOINCREMENT,
+            ServerID INTEGER NOT NULL,
+            Key TEXT NOT NULL,
+            Value TEXT NOT NULL,
+            FOREIGN KEY(ServerID) REFERENCES Server(ID),
+            UNIQUE(ServerID, Key)
+          )`,
+          []
+        );
+
+        await this.run(
+          `CREATE TABLE ServerDataEntry (
+            ID INTEGER PRIMARY KEY AUTOINCREMENT,
+            ServerID INTEGER NOT NULL,
+            Timestamp INTEGER NOT NULL,
+            FOREIGN KEY(ServerID) REFERENCES Server(ID),
+            UNIQUE(ServerID, Timestamp)
+           );`,
+          []
+        );
+
+        await this.run(
+          `CREATE TABLE ServerDataValue (
+            ID INTEGER PRIMARY KEY AUTOINCREMENT,
+            EntryID INTEGER NOT NULL,
+            Key TEXT NOT NULL,
+            Value REAL NOT NULL,
+            FOREIGN KEY(EntryID) REFERENCES ServerDataEntry(ID),
+            UNIQUE(EntryID, Key)
+          );`,
+          []
+        );
+
+        let result = await this.run(`INSERT INTO Server(Title, FQDN) VALUES(?, ?);`, ['Raspi4', '10.8.0.10']);
+        const serverID = result.lastID;
+        Logger.debug(`[DEBUG] Created Server #${serverID}`);
+        result = await this.run(`INSERT INTO ServerConfig(ServerID, Key, Value) VALUES(?, ?, ?);`, [serverID, 'syncInterval', 300]);
+      }
+    } catch (err) {
+      Logger.error('[FATAL] Initializing Database failed:', err);
+      Logger.error('[EXITING]');
+      process.exit(1);
+    }
+  }
+
+  public async getAllServerConfigs(): Promise<Server[]> {
+    const res = await this.stmt(
+      `SELECT 
+        Server.*,
+        ServerConfig.Key,
+        ServerConfig.Value
+        FROM Server
+        LEFT OUTER JOIN ServerConfig ON Server.ID = ServerConfig.ServerID
+        ORDER BY Server.Title, ServerConfig.Key`,
+      []
+    );
+
+    return res.rows.reduce((res: Server[], line, i) => {
+      const serverID = line['ID'];
+      let server: Server;
+      if (i === 0 || res[res.length - 1].id !== serverID) {
+        server = { id: serverID, title: line['Title'], fqdn: line['FQDN'], config: {} };
+        res.push(server);
+      } else {
+        server = res[res.length - 1];
+      }
+
+      if (!!line['Key']) {
+        server.config[line['Key']] = line['Value'];
+      }
+
+      return res;
+    }, [] as Server[]);
+  }
+
+  private async run(sql: string, params: any): Promise<RunResult> {
+    return new Promise<RunResult>((res, rej) => {
+      this.db.run(sql, params, function (err) {
+        if (err) return rej(err);
+        res(this);
+      });
+    });
+  }
+
+  private async stmt(sql: string, params: any[]) {
+    return new Promise<{ result: RunResult; rows: any[] }>((res, rej) => {
+      const stmt = this.db.prepare(sql);
+      stmt.all(params, function (err, rows) {
+        if (err) return rej(err);
+        res({ result: this, rows });
+      });
+    });
+  }
+}

+ 10 - 1
server/src/index.ts

@@ -1,11 +1,20 @@
 import dotenv from 'dotenv';
 
 import { Logger, LogLevel } from '../../common/util/logger.class';
+import { Database } from './database.class';
 import { Webserver } from './webserver.class';
+import { ServerConnector } from './server-connector.class';
+import { Timer } from './timer.class';
 
 dotenv.config();
 
 const LOG_LEVEL: LogLevel = (process.env.LOG_LEVEL as LogLevel) || 'INFO';
 Logger.logLevel = LOG_LEVEL;
 
-new Webserver(Number(process.env.WEB_PORT ?? '80'));
+(async () => {
+  const db = new Database();
+  await db.open();
+  Timer.instance.start();
+  new Webserver(Number(process.env.WEB_PORT ?? '80'), db);
+  new ServerConnector(db);
+})();

+ 30 - 0
server/src/server-connector.class.ts

@@ -0,0 +1,30 @@
+import { Logger } from '../../common/util/logger.class';
+import { Database } from './database.class';
+import { Timer } from './timer.class';
+
+export class ServerConnector {
+  private subscriptions: Array<{ id: number; interval: number; server: Server }> = [];
+
+  constructor(private db: Database) {
+    (async () => {
+      try {
+        const serverList = await db.getAllServerConfigs();
+
+        serverList.forEach(server => {
+          const interval = Number(server.config['syncInterval'] ?? '300');
+          Logger.info('[INFO] Starting Server Sync Connector for', server.title, 'with interval', interval, 'seconds ...');
+          const id = Timer.instance.subscribe(interval, () => this.timerTick(server));
+          this.subscriptions.push({ id, interval, server });
+        });
+      } catch (err) {
+        Logger.error('[FATAL] Initializing ServerConnector failed:', err);
+        Logger.error('[EXITING]');
+        process.exit(1);
+      }
+    })();
+  }
+
+  private timerTick(server: Server) {
+    Logger.debug('[DEBUG] TICK', new Date(), JSON.stringify(server));
+  }
+}

+ 59 - 0
server/src/timer.class.ts

@@ -0,0 +1,59 @@
+export class Timer {
+  private intervalHdl?: NodeJS.Timer;
+  private subscribers: {
+    [id: number]: {
+      lastTick: number;
+      seconds: number;
+      tick: () => void;
+    };
+  } = {};
+
+  private static INSTANCE?: Timer;
+  public static get instance(): Timer {
+    if (!Timer.INSTANCE) {
+      Timer.INSTANCE = new Timer();
+    }
+    return Timer.INSTANCE;
+  }
+
+  private constructor() {}
+
+  public start() {
+    if (this.intervalHdl) {
+      return;
+    }
+    this.intervalHdl = setInterval(this.loop.bind(this), 1000);
+  }
+
+  private loop() {
+    const now = new Date();
+    Object.values(this.subscribers).forEach(sub => {
+      if (now.getTime() >= sub.lastTick + sub.seconds * 1000) {
+        sub.lastTick = now.getTime();
+        sub.tick();
+      }
+    });
+  }
+
+  public subscribe(seconds: number, tick: () => void) {
+    const lastTick = new Date().getTime();
+    const id =
+      Object.keys(this.subscribers)
+        .map(k => Number(k))
+        .reduce((r, id) => Math.max(r, id), 0) + 1;
+    this.subscribers[id] = { lastTick, seconds, tick };
+    return id;
+  }
+
+  public unsubscribe(id: number) {
+    if (typeof this.subscribers[id] === 'function') {
+      delete this.subscribers[id];
+    }
+  }
+
+  public stop() {
+    if (!this.intervalHdl) return;
+    clearInterval(this.intervalHdl);
+    this.intervalHdl = undefined;
+  }
+}

+ 2 - 1
server/src/webserver.class.ts

@@ -1,10 +1,11 @@
 import express, { Express } from 'express';
 import { Logger } from '../../common/util/logger.class';
+import { Database } from './database.class';
 
 export class Webserver {
   private app: Express;
 
-  constructor(private port: number) {
+  constructor(private port: number, private db: Database) {
     this.app = express();
 
     this.app.use('/', express.static(process.env.STATIC_DIR || 'public'));