UNPKG

node-karin

Version:

Lightweight, efficient, concise, and stable robot framework.

661 lines (657 loc) 23.3 kB
import os from 'node:os'; import fs from 'node:fs'; import require$$0 from 'fs'; import require$$1 from 'path'; import require$$2 from 'os'; import require$$3 from 'crypto'; import path from 'node:path'; import { createServer } from 'node:net'; import { fileURLToPath } from 'node:url'; import { fork } from 'node:child_process'; // src/start/index.ts function _mergeNamespaces(n, m) { for (var i = 0; i < m.length; i++) { const e = m[i]; if (typeof e !== "string" && !Array.isArray(e)) { for (const k in e) { if (k !== "default" && !(k in n)) { const d = Object.getOwnPropertyDescriptor(e, k); if (d) { Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: () => e[k] }); } } } } } return Object.freeze(Object.defineProperty(n, Symbol.toStringTag, { value: "Module" })); } function getDefaultExportFromCjs(x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x; } var main$1 = { exports: {} }; var version = "16.5.0"; var require$$4 = { version }; var hasRequiredMain; function requireMain() { if (hasRequiredMain) return main$1.exports; hasRequiredMain = 1; const fs2 = require$$0; const path2 = require$$1; const os2 = require$$2; const crypto = require$$3; const packageJson = require$$4; const version2 = packageJson.version; const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg; function parse2(src) { const obj = {}; let lines = src.toString(); lines = lines.replace(/\r\n?/mg, "\n"); let match; while ((match = LINE.exec(lines)) != null) { const key = match[1]; let value = match[2] || ""; value = value.trim(); const maybeQuote = value[0]; value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2"); if (maybeQuote === '"') { value = value.replace(/\\n/g, "\n"); value = value.replace(/\\r/g, "\r"); } obj[key] = value; } return obj; } function _parseVault2(options) { const vaultPath = _vaultPath(options); const result = DotenvModule.configDotenv({ path: vaultPath }); if (!result.parsed) { const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`); err.code = "MISSING_DATA"; throw err; } const keys = _dotenvKey(options).split(","); const length = keys.length; let decrypted; for (let i = 0; i < length; i++) { try { const key = keys[i].trim(); const attrs = _instructions(result, key); decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key); break; } catch (error) { if (i + 1 >= length) { throw error; } } } return DotenvModule.parse(decrypted); } function _warn(message) { console.log(`[dotenv@${version2}][WARN] ${message}`); } function _debug(message) { console.log(`[dotenv@${version2}][DEBUG] ${message}`); } function _dotenvKey(options) { if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) { return options.DOTENV_KEY; } if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) { return process.env.DOTENV_KEY; } return ""; } function _instructions(result, dotenvKey) { let uri; try { uri = new URL(dotenvKey); } catch (error) { if (error.code === "ERR_INVALID_URL") { const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development"); err.code = "INVALID_DOTENV_KEY"; throw err; } throw error; } const key = uri.password; if (!key) { const err = new Error("INVALID_DOTENV_KEY: Missing key part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environment = uri.searchParams.get("environment"); if (!environment) { const err = new Error("INVALID_DOTENV_KEY: Missing environment part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`; const ciphertext = result.parsed[environmentKey]; if (!ciphertext) { const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`); err.code = "NOT_FOUND_DOTENV_ENVIRONMENT"; throw err; } return { ciphertext, key }; } function _vaultPath(options) { let possibleVaultPath = null; if (options && options.path && options.path.length > 0) { if (Array.isArray(options.path)) { for (const filepath of options.path) { if (fs2.existsSync(filepath)) { possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`; } } } else { possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`; } } else { possibleVaultPath = path2.resolve(process.cwd(), ".env.vault"); } if (fs2.existsSync(possibleVaultPath)) { return possibleVaultPath; } return null; } function _resolveHome(envPath) { return envPath[0] === "~" ? path2.join(os2.homedir(), envPath.slice(1)) : envPath; } function _configVault2(options) { const debug = Boolean(options && options.debug); if (debug) { _debug("Loading env from encrypted .env.vault"); } const parsed = DotenvModule._parseVault(options); let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsed, options); return { parsed }; } function configDotenv2(options) { const dotenvPath = path2.resolve(process.cwd(), ".env"); let encoding = "utf8"; const debug = Boolean(options && options.debug); if (options && options.encoding) { encoding = options.encoding; } else { if (debug) { _debug("No encoding is specified. UTF-8 is used by default"); } } let optionPaths = [dotenvPath]; if (options && options.path) { if (!Array.isArray(options.path)) { optionPaths = [_resolveHome(options.path)]; } else { optionPaths = []; for (const filepath of options.path) { optionPaths.push(_resolveHome(filepath)); } } } let lastError; const parsedAll = {}; for (const path22 of optionPaths) { try { const parsed = DotenvModule.parse(fs2.readFileSync(path22, { encoding })); DotenvModule.populate(parsedAll, parsed, options); } catch (e) { if (debug) { _debug(`Failed to load ${path22} ${e.message}`); } lastError = e; } } let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsedAll, options); if (lastError) { return { parsed: parsedAll, error: lastError }; } else { return { parsed: parsedAll }; } } function config2(options) { if (_dotenvKey(options).length === 0) { return DotenvModule.configDotenv(options); } const vaultPath = _vaultPath(options); if (!vaultPath) { _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`); return DotenvModule.configDotenv(options); } return DotenvModule._configVault(options); } function decrypt2(encrypted, keyStr) { const key = Buffer.from(keyStr.slice(-64), "hex"); let ciphertext = Buffer.from(encrypted, "base64"); const nonce = ciphertext.subarray(0, 12); const authTag = ciphertext.subarray(-16); ciphertext = ciphertext.subarray(12, -16); try { const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce); aesgcm.setAuthTag(authTag); return `${aesgcm.update(ciphertext)}${aesgcm.final()}`; } catch (error) { const isRange = error instanceof RangeError; const invalidKeyLength = error.message === "Invalid key length"; const decryptionFailed = error.message === "Unsupported state or unable to authenticate data"; if (isRange || invalidKeyLength) { const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)"); err.code = "INVALID_DOTENV_KEY"; throw err; } else if (decryptionFailed) { const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY"); err.code = "DECRYPTION_FAILED"; throw err; } else { throw error; } } } function populate2(processEnv, parsed, options = {}) { const debug = Boolean(options && options.debug); const override = Boolean(options && options.override); if (typeof parsed !== "object") { const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate"); err.code = "OBJECT_REQUIRED"; throw err; } for (const key of Object.keys(parsed)) { if (Object.prototype.hasOwnProperty.call(processEnv, key)) { if (override === true) { processEnv[key] = parsed[key]; } if (debug) { if (override === true) { _debug(`"${key}" is already defined and WAS overwritten`); } else { _debug(`"${key}" is already defined and was NOT overwritten`); } } } else { processEnv[key] = parsed[key]; } } } const DotenvModule = { configDotenv: configDotenv2, _configVault: _configVault2, _parseVault: _parseVault2, config: config2, decrypt: decrypt2, parse: parse2, populate: populate2 }; main$1.exports.configDotenv = DotenvModule.configDotenv; main$1.exports._configVault = DotenvModule._configVault; main$1.exports._parseVault = DotenvModule._parseVault; main$1.exports.config = DotenvModule.config; main$1.exports.decrypt = DotenvModule.decrypt; main$1.exports.parse = DotenvModule.parse; main$1.exports.populate = DotenvModule.populate; main$1.exports = DotenvModule; return main$1.exports; } var mainExports = requireMain(); var main = /* @__PURE__ */ getDefaultExportFromCjs(mainExports); var dotenv = /* @__PURE__ */ _mergeNamespaces({ __proto__: null, default: main }, [mainExports]); // ../../node_modules/.pnpm/@karinjs+dotenv@1.1.2/node_modules/@karinjs/dotenv/dist/index.js var { // @ts-ignore _configVault, _parseVault } = dotenv; mainExports.config; mainExports.configDotenv; mainExports.decrypt; mainExports.parse; mainExports.populate; // ../../node_modules/.pnpm/@karinjs+dotenv@1.1.2/node_modules/@karinjs/dotenv/index.js var app = dotenv.default; var dotenv_default = app; var ProcessManager = class { /** 是否已经启动 */ isStarted = false; /** 子进程实例 */ childProcess = null; /** 重启延迟时间 */ RESTART_DELAY_MS = 200; /** 模块名称 */ MODULE_NAME = "ProcessManager"; /** 启动时间 */ startTime = Date.now(); /** 进程统计信息 */ processStats = { /** 重启次数 */ restartCount: 0, /** 最后重启时间 */ lastRestartTime: 0 }; /** 默认HTTP端口 */ DEFAULT_HTTP_PORT = 7777; // ANSI 颜色代码 COLORS = { green: "\x1B[32m", yellow: "\x1B[33m", red: "\x1B[31m", blue: "\x1B[34m", magenta: "\x1B[35m", cyan: "\x1B[36m", gray: "\x1B[90m", reset: "\x1B[0m" }; /** exit 事件监听器引用 */ exitListenerRef = null; constructor() { process.on("exit", this.stop.bind(this)); this.logSystemInfo(); } /** * 启动子进程 */ start(options = {}) { if (this.isAlreadyRunning()) { this.log(`\u5B50\u8FDB\u7A0B\u5DF2\u5728\u8FD0\u884C | PID: ${this.childProcess.pid}`); return this.childProcess; } this.isStarted = true; const entryPath = this.resolveEntryPath(); const args = options.reloadDeps ? [`--reload-timestamp=${Date.now()}`] : []; this.childProcess = fork(entryPath, args); const pid = this.childProcess.pid; this.setupEventListeners(); this.log(`\u542F\u52A8\u5B50\u8FDB\u7A0B | PID: ${pid} | \u5165\u53E3: ${path.basename(entryPath)} | \u53C2\u6570: ${args.join(" ") || "\u65E0"}`); return this.childProcess; } /** * 重启子进程 */ async restart(reloadDeps = false) { if (!this.childProcess) { this.logWarn("\u91CD\u542F\u5931\u8D25 | \u539F\u56E0: \u5B50\u8FDB\u7A0B\u4E0D\u5B58\u5728 | \u64CD\u4F5C: \u76F4\u63A5\u542F\u52A8\u65B0\u8FDB\u7A0B"); return this.start({ reloadDeps }); } const oldPid = this.childProcess.pid; this.processStats.restartCount++; this.processStats.lastRestartTime = Date.now(); this.log(`\u51C6\u5907\u91CD\u542F\u5B50\u8FDB\u7A0B | \u5F53\u524DPID: ${oldPid} | \u91CD\u8F7D\u4F9D\u8D56: ${reloadDeps ? "\u662F" : "\u5426"} | \u91CD\u542F\u6B21\u6570: ${this.processStats.restartCount}`); try { await this.terminateChildProcess(); this.isStarted = false; const newChild = this.start({ reloadDeps }); const duration = Date.now() - this.processStats.lastRestartTime; this.log(`\u91CD\u542F\u5B8C\u6210 | \u65E7PID: ${oldPid} | \u65B0PID: ${newChild.pid} | \u8017\u65F6: ${duration}ms`); return newChild; } catch (error) { if (error.code === "ESRCH") { this.logWarn(`\u91CD\u542F\u8FC7\u7A0B\u4E2D\u53D1\u73B0\u8FDB\u7A0B\u5DF2\u4E0D\u5B58\u5728 | \u9519\u8BEF\u7801: ${error.code} | \u64CD\u4F5C: \u542F\u52A8\u65B0\u8FDB\u7A0B`); this.isStarted = false; return this.start({ reloadDeps: true }); } this.logError(`\u91CD\u542F\u5931\u8D25 | \u9519\u8BEF: ${error.message} | \u4EE3\u7801: ${error.code || "\u672A\u77E5"}`); return null; } } /** * 停止子进程并退出父进程 */ stop() { if (this.childProcess) { const pid = this.childProcess.pid; const uptime = this.getProcessUptime(); this.log(`\u6B63\u5728\u5173\u95ED\u5B50\u8FDB\u7A0B | PID: ${pid} | \u8FD0\u884C\u65F6\u95F4: ${uptime}`); this.terminateChildProcess(); this.childProcess = null; this.isStarted = false; } const totalUptime = this.getTotalUptime(); this.log(`\u7236\u8FDB\u7A0B\u9000\u51FA | \u603B\u8FD0\u884C\u65F6\u95F4: ${totalUptime} | \u91CD\u542F\u6B21\u6570: ${this.processStats.restartCount}`); process.exit(0); } /** * 检查子进程是否已经在运行 */ isAlreadyRunning() { return this.isStarted && this.childProcess !== null; } /** * 设置子进程事件监听器 */ setupEventListeners() { if (!this.childProcess) return; this.childProcess.on("message", this.handleChildMessage.bind(this)); if (this.exitListenerRef) { this.childProcess.off("exit", this.exitListenerRef); } this.exitListenerRef = this.handleChildExit.bind(this); this.childProcess.once("exit", this.exitListenerRef); } /** * 处理子进程发送的消息 */ async handleChildMessage(message) { if (typeof message !== "string") return; try { const { type, reloadDeps } = JSON.parse(message); if (type === "restart" || type === "rs") { this.log(`\u6536\u5230\u6D88\u606F | \u7C7B\u578B: restart | \u91CD\u8F7D\u4F9D\u8D56: ${reloadDeps ? "\u662F" : "\u5426"}`); const child = await this.restart(reloadDeps); if (!child) { this.logWarn("\u91CD\u542F\u64CD\u4F5C\u5931\u8D25 | \u65E0\u6CD5\u83B7\u53D6\u65B0\u7684\u5B50\u8FDB\u7A0B"); } else { this.childProcess = child; } return; } if (type === "stop") { this.log("\u6536\u5230\u6D88\u606F | \u7C7B\u578B: stop | \u64CD\u4F5C: \u505C\u6B62\u8FDB\u7A0B"); this.stop(); return; } this.logWarn(`\u6536\u5230\u672A\u77E5\u6D88\u606F | \u7C7B\u578B: ${type || "\u672A\u77E5"} | \u5185\u5BB9: ${JSON.stringify(message).substring(0, 100)}`); } catch (error) { this.logError(`\u6D88\u606F\u5904\u7406\u9519\u8BEF | \u539F\u59CB\u6D88\u606F: ${String(message).substring(0, 50)}`, error); } } /** * 处理子进程退出事件 */ handleChildExit(code, signal) { const exitType = signal ? `\u4FE1\u53F7: ${signal}` : `\u9000\u51FA\u7801: ${code ?? "\u672A\u77E5"}`; const uptime = this.getProcessUptime(); this.log(`\u5B50\u8FDB\u7A0B\u9000\u51FA | ${exitType} | \u8FD0\u884C\u65F6\u95F4: ${uptime}`); this.stop(); } /** * 等待端口释放 * @param port - 需要检查的端口号 * @param maxAttempts - 最大尝试次数 * @param interval - 检查间隔(ms) * @returns 端口是否可用 */ async waitForPortRelease(port, maxAttempts = 30, interval = 500) { const checkPort = (port2) => { return new Promise((resolve) => { const server = createServer(); server.once("error", () => { server.close(); resolve(false); }); server.once("listening", () => { server.close(); resolve(true); }); server.listen(port2, "127.0.0.1"); }); }; const isProcessAlive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } }; for (let attempt = 0; attempt < maxAttempts; attempt++) { if (this.childProcess?.pid && !isProcessAlive(this.childProcess.pid)) { this.log(`\u5B50\u8FDB\u7A0B\u5DF2\u9000\u51FA | PID: ${this.childProcess.pid}`); const portAvailable2 = await checkPort(port); if (portAvailable2) { this.log(`\u7AEF\u53E3 ${port} \u5DF2\u91CA\u653E\uFF0C\u53EF\u4EE5\u4F7F\u7528`); return true; } else { this.logWarn(`\u5B50\u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF0C\u4F46\u7AEF\u53E3 ${port} \u4ECD\u88AB\u5360\u7528 (\u53EF\u80FD\u88AB\u5176\u4ED6\u8FDB\u7A0B\u5360\u7528)`); } } const portAvailable = await checkPort(port); if (portAvailable) { this.log(`\u7AEF\u53E3 ${port} \u5DF2\u91CA\u653E\uFF0C\u53EF\u4EE5\u4F7F\u7528`); return true; } this.logWarn(`\u7AEF\u53E3 ${port} \u4ECD\u88AB\u5360\u7528\uFF0C\u7B49\u5F85\u91CA\u653E... (${attempt + 1}/${maxAttempts})`); try { if (this?.childProcess?.pid && isProcessAlive(this.childProcess.pid)) { this.log(`\u5C1D\u8BD5\u7EC8\u6B62\u5B50\u8FDB\u7A0B | PID: ${this.childProcess.pid}`); process.kill(this.childProcess.pid); } } catch { } await new Promise((resolve) => setTimeout(resolve, interval)); } this.logError(`\u7AEF\u53E3 ${port} \u5728 ${maxAttempts} \u6B21\u5C1D\u8BD5\u540E\u4ECD\u88AB\u5360\u7528`); return false; } /** * 获取HTTP端口号 * @returns HTTP端口号 */ getHttpPort() { try { const envPath = path.resolve(process.cwd(), ".env"); if (fs.existsSync(envPath)) { const envConfig = dotenv_default.parse(fs.readFileSync(envPath)); if (envConfig.HTTP_PORT) { return Number(envConfig.HTTP_PORT); } } return this.DEFAULT_HTTP_PORT; } catch (error) { this.logWarn(`\u8BFB\u53D6HTTP\u7AEF\u53E3\u5931\u8D25: ${error.message}\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u7AEF\u53E3${this.DEFAULT_HTTP_PORT}`); return this.DEFAULT_HTTP_PORT; } } /** * 终止子进程 */ async terminateChildProcess() { if (!this.childProcess) return; if (this.exitListenerRef) { this.childProcess.off("exit", this.exitListenerRef); this.exitListenerRef = null; } try { const pid = this.childProcess.pid; this.log(`\u53D1\u9001\u7EC8\u6B62\u4FE1\u53F7 | PID: ${pid} | \u4FE1\u53F7: SIGTERM`); this.childProcess.kill("SIGTERM"); this.log(`\u7B49\u5F85\u8FDB\u7A0B\u7EC8\u6B62 | PID: ${pid} | \u8D85\u65F6: ${this.RESTART_DELAY_MS}ms`); const port = this.getHttpPort(); await this.waitForPortRelease(port); try { if (this?.childProcess?.pid) { this.log(`\u786E\u4FDD\u8FDB\u7A0B\u7EC8\u6B62 | PID: ${pid} | \u53D1\u9001\u5F3A\u5236\u7EC8\u6B62\u4FE1\u53F7`); process.kill(this.childProcess.pid); } } catch { } } catch { } } /** * 解析子进程入口文件路径 */ resolveEntryPath() { const currentFilePath = fileURLToPath(import.meta.url); const currentDirPath = path.dirname(currentFilePath); const isESM = import.meta.url.includes(".mjs"); const localEntryPath = path.join(currentDirPath, isESM ? "app.mjs" : "app.ts"); return fs.existsSync(localEntryPath) ? localEntryPath : path.join(process.cwd(), "node_modules", "node-karin", "dist", "start", "app.mjs"); } /** * 获取当前时间戳,格式为 HH:MM:SS.mmm */ getTimestamp() { const now = /* @__PURE__ */ new Date(); const hours = now.getHours().toString().padStart(2, "0"); const minutes = now.getMinutes().toString().padStart(2, "0"); const seconds = now.getSeconds().toString().padStart(2, "0"); const milliseconds = now.getMilliseconds().toString().padStart(3, "0"); return `${hours}:${minutes}:${seconds}.${milliseconds}`; } /** * 创建日志前缀 */ createLogPrefix(level) { const timestamp = this.getTimestamp(); const prefix = `[Karin][${timestamp}][${level}]`; return `${this.COLORS.green}${prefix}${this.COLORS.reset}`; } /** * 获取进程运行时间的友好字符串 */ getProcessUptime() { if (!this.childProcess?.pid) return "0s"; const uptime = (Date.now() - this.processStats.lastRestartTime) / 1e3; if (uptime < 60) return `${uptime.toFixed(1)}s`; if (uptime < 3600) return `${(uptime / 60).toFixed(1)}m`; return `${(uptime / 3600).toFixed(1)}h`; } /** * 获取总运行时间的友好字符串 */ getTotalUptime() { const uptime = (Date.now() - this.startTime) / 1e3; if (uptime < 60) return `${uptime.toFixed(1)}s`; if (uptime < 3600) return `${(uptime / 60).toFixed(1)}m`; return `${(uptime / 3600).toFixed(1)}h`; } /** * 输出系统信息 */ logSystemInfo() { const nodeVersion = process.version; const platform = `${os.platform()} ${os.release()}`; const cpus = os.cpus().length; const memory = `${Math.round(os.totalmem() / 1024 / 1024 / 1024)}GB`; this.log(`\u7CFB\u7EDF\u4FE1\u606F | Node: ${nodeVersion} | \u5E73\u53F0: ${platform} | CPU\u6838\u5FC3: ${cpus} | \u5185\u5B58: ${memory}`); } /** * 日志输出方法 */ log(message) { console.log(`${this.createLogPrefix("INFO" /* INFO */)} [${this.MODULE_NAME}] ${message}`); } logWarn(message) { console.warn(`${this.createLogPrefix("WARN" /* WARN */)} [${this.MODULE_NAME}] ${message}`); } logError(message, error) { console.error(`${this.createLogPrefix("ERROR" /* ERROR */)} [${this.MODULE_NAME}] ${message}`, error || ""); } // private logDebug(message: string): void { // console.debug(`${this.createLogPrefix(LogLevel.DEBUG)} [${this.MODULE_NAME}] ${message}`) // } }; new ProcessManager().start();