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} 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} */ 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, };