• embedded-mariadb.js
  • const { log } = require("../src/util");
    const childProcess = require("child_process");
    const fs = require("fs");
    const mysql = require("mysql2");
    
    /**
     * It is only used inside the docker container
     */
    class EmbeddedMariaDB {
    
        static instance = null;
    
        exec = "mariadbd";
    
        mariadbDataDir = "/app/data/mariadb";
    
        runDir = "/app/data/run";
    
        socketPath = this.runDir + "/mariadb.sock";
    
        /**
         * The username to connect to the MariaDB
         * @type {string}
         */
        username = null;
    
        /**
         * @type {ChildProcessWithoutNullStreams}
         * @private
         */
        childProcess = null;
        running = false;
    
        started = false;
    
        /**
         * @returns {EmbeddedMariaDB} The singleton instance
         */
        static getInstance() {
            if (!EmbeddedMariaDB.instance) {
                EmbeddedMariaDB.instance = new EmbeddedMariaDB();
            }
            return EmbeddedMariaDB.instance;
        }
    
        /**
         * @returns {boolean} If the singleton instance is created
         */
        static hasInstance() {
            return !!EmbeddedMariaDB.instance;
        }
    
        /**
         * Start the embedded MariaDB
         * @throws {Error} If the current user is not "node" or "root"
         * @returns {Promise<void>|void} A promise that resolves when the MariaDB is started or void if it is already started
         */
        start() {
            // Check if the current user is "node" or "root"
            this.username = require("os").userInfo().username;
            if (this.username !== "node" && this.username !== "root") {
                throw new Error("Embedded Mariadb supports only 'node' or 'root' user, but the current user is: " + this.username);
            }
    
            this.initDB();
    
            this.startChildProcess();
    
            return new Promise((resolve) => {
                let interval = setInterval(() => {
                    if (this.started) {
                        clearInterval(interval);
                        resolve();
                    } else {
                        log.info("mariadb", "Waiting for Embedded MariaDB to start...");
                    }
                }, 1000);
            });
        }
    
        /**
         * Start the child process
         * @returns {void}
         */
        startChildProcess() {
            if (this.childProcess) {
                log.info("mariadb", "Already started");
                return;
            }
    
            this.running = true;
            log.info("mariadb", "Starting Embedded MariaDB");
            this.childProcess = childProcess.spawn(this.exec, [
                "--user=node",
                "--datadir=" + this.mariadbDataDir,
                `--socket=${this.socketPath}`,
                `--pid-file=${this.runDir}/mysqld.pid`,
                // Don't add the following option, the mariadb will not report message to the console, which affects initDBAfterStarted()
                // "--log-error=" + `${this.mariadbDataDir}/mariadb-error.log`,
            ]);
    
            this.childProcess.on("close", (code) => {
                this.running = false;
                this.childProcess = null;
                this.started = false;
                log.info("mariadb", "Stopped Embedded MariaDB: " + code);
    
                if (code !== 0) {
                    log.error("mariadb", "Try to restart Embedded MariaDB as it is not stopped by user");
                    this.startChildProcess();
                }
            });
    
            this.childProcess.on("error", (err) => {
                if (err.code === "ENOENT") {
                    log.error("mariadb", `Embedded MariaDB: ${this.exec} is not found`);
                } else {
                    log.error("mariadb", err);
                }
            });
    
            let handler = (data) => {
                log.info("mariadb", data.toString("utf-8"));
                if (data.toString("utf-8").includes("ready for connections")) {
                    this.initDBAfterStarted();
                }
            };
    
            this.childProcess.stdout.on("data", handler);
            this.childProcess.stderr.on("data", handler);
        }
    
        /**
         * Stop all the child processes
         * @returns {void}
         */
        stop() {
            if (this.childProcess) {
                this.childProcess.kill("SIGINT");
                this.childProcess = null;
            }
        }
    
        /**
         * Install MariaDB if it is not installed and make sure the `runDir` directory exists
         * @returns {void}
         */
        initDB() {
            if (!fs.existsSync(this.mariadbDataDir)) {
                log.info("mariadb", `Embedded MariaDB: ${this.mariadbDataDir} is not found, create one now.`);
                fs.mkdirSync(this.mariadbDataDir, {
                    recursive: true,
                });
    
                let result = childProcess.spawnSync("mariadb-install-db", [
                    "--user=node",
                    "--auth-root-socket-user=node",
                    "--datadir=" + this.mariadbDataDir,
                    "--auth-root-authentication-method=socket",
                ]);
    
                if (result.status !== 0) {
                    let error = result.stderr.toString("utf-8");
                    log.error("mariadb", error);
                    return;
                } else {
                    log.info("mariadb", "Embedded MariaDB: mysql_install_db done:" + result.stdout.toString("utf-8"));
                }
            }
    
            // Check the owner of the mariadb directory, and change it if necessary
            let stat = fs.statSync(this.mariadbDataDir);
            if (stat.uid !== 1000 || stat.gid !== 1000) {
                fs.chownSync(this.mariadbDataDir, 1000, 1000);
            }
    
            // Check the permission of the mariadb directory, and change it if it is not 755
            if (stat.mode !== 0o755) {
                fs.chmodSync(this.mariadbDataDir, 0o755);
            }
    
            if (!fs.existsSync(this.runDir)) {
                log.info("mariadb", `Embedded MariaDB: ${this.runDir} is not found, create one now.`);
                fs.mkdirSync(this.runDir, {
                    recursive: true,
                });
            }
    
            stat = fs.statSync(this.runDir);
            if (stat.uid !== 1000 || stat.gid !== 1000) {
                fs.chownSync(this.runDir, 1000, 1000);
            }
            if (stat.mode !== 0o755) {
                fs.chmodSync(this.runDir, 0o755);
            }
        }
    
        /**
         * Initialise the "kuma" database in mariadb if it does not exist
         * @returns {Promise<void>}
         */
        async initDBAfterStarted() {
            const connection = mysql.createConnection({
                socketPath: this.socketPath,
                user: this.username,
            });
    
            let result = await connection.execute("CREATE DATABASE IF NOT EXISTS `kuma`");
            log.debug("mariadb", "CREATE DATABASE: " + JSON.stringify(result));
    
            log.info("mariadb", "Embedded MariaDB is ready for connections");
            this.started = true;
        }
    
    }
    
    module.exports = {
        EmbeddedMariaDB,
    };