Compare commits
17 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
3efd2fe9dd | |
|
|
eb39114fe9 | |
|
|
152d20efe4 | |
|
|
96d4337b59 | |
|
|
fc5f551cab | |
|
|
89fe961434 | |
|
|
3c32d41347 | |
|
|
f3e6ed4917 | |
|
|
13384ab772 | |
|
|
fb727c6b3c | |
|
|
35095a0219 | |
|
|
09d14be5e7 | |
|
|
22289f04d4 | |
|
|
52b3ed8d97 | |
|
|
3e0dba7172 | |
|
|
3781ed4629 | |
|
|
0e023f9a88 |
|
|
@ -23,7 +23,6 @@ const koa_mount_1 = tslib_1.__importDefault(require("koa-mount"));
|
|||
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
||||
const utils_1 = require("../utils");
|
||||
const bcryptjs_1 = tslib_1.__importDefault(require("bcryptjs"));
|
||||
const polyfill_1 = require("./polyfill");
|
||||
(0, utils_1.checkNodeVersion)();
|
||||
const socketAdminUI = (0, path_1.join)(__dirname, '../../ui/socket-admin');
|
||||
const DATA_SUBSCRIBE_NAMESPACE = '/dsn';
|
||||
|
|
@ -173,7 +172,7 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
|
|||
if (routine) {
|
||||
// 如果传入了routine,执行完成后就结束
|
||||
const result = await appLoader.execRoutine(routine);
|
||||
await appLoader.unmount();
|
||||
// await appLoader.unmount(); // 不卸载,在进程退出时会自动卸载
|
||||
return result;
|
||||
}
|
||||
// if (errorHandler && typeof errorHandler === 'function') {
|
||||
|
|
@ -200,12 +199,16 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
|
|||
catch (err) {
|
||||
console.error(err);
|
||||
const { request } = ctx;
|
||||
const exception = err instanceof types_1.OakException
|
||||
const exception = (0, types_1.isOakException)(err)
|
||||
? err
|
||||
: new types_1.OakException(serverConfiguration?.internalExceptionMask ||
|
||||
ExceptionMask);
|
||||
const { body } = connector.serializeException(exception, request.headers, request.body);
|
||||
const { body, headers } = connector.serializeException(exception, request.headers, request.body);
|
||||
ctx.response.body = body;
|
||||
// headers 要拼上
|
||||
Object.keys(headers || {}).forEach(key => {
|
||||
ctx.set(key, headers?.[key]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
|
@ -236,6 +239,10 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
|
|||
ctx.set('Access-Control-Allow-Origin', '*');
|
||||
ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader()));
|
||||
ctx.set('Access-Control-Allow-Methods', corsMethods);
|
||||
if (connector.getCorsExposeHeaders) {
|
||||
const exposeHeaders = connector.getCorsExposeHeaders();
|
||||
ctx.set('Access-Control-Expose-Headers', exposeHeaders);
|
||||
}
|
||||
if (ctx.method == 'OPTIONS') {
|
||||
ctx.body = 200;
|
||||
}
|
||||
|
|
@ -269,12 +276,39 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
|
|||
}
|
||||
});
|
||||
}
|
||||
const connectorCustomAspects = connector.registerCustomAspect ? connector.registerCustomAspect() : null;
|
||||
router.post(connector.getRouter(), async (ctx) => {
|
||||
const { request } = ctx;
|
||||
const { contextString, aspectName, data } = connector.parseRequest(request.headers, request.body, request.files);
|
||||
const { result, opRecords, message } = await appLoader.execAspect(aspectName, request.headers, contextString, data);
|
||||
const { contextString, aspectName, data } = await connector.parseRequest(request.headers, request.body, request.files);
|
||||
let result;
|
||||
let opRecords = [];
|
||||
let message = undefined;
|
||||
if (connectorCustomAspects &&
|
||||
connectorCustomAspects.findIndex(a => a.name === aspectName) >= 0) {
|
||||
// 自定义aspect处理
|
||||
console.log(`调用Connector自定义Aspect: ${aspectName}`);
|
||||
const aspect = connectorCustomAspects.find(a => a.name === aspectName);
|
||||
const res = await aspect.handler({
|
||||
headers: request.headers,
|
||||
contextString,
|
||||
params: data,
|
||||
});
|
||||
result = res.result;
|
||||
opRecords = res.opRecords || [];
|
||||
message = res.message;
|
||||
}
|
||||
else {
|
||||
const res = await appLoader.execAspect(aspectName, request.headers, contextString, data);
|
||||
result = res.result;
|
||||
opRecords = res.opRecords || [];
|
||||
message = res.message;
|
||||
}
|
||||
const { body, headers } = await connector.serializeResult(result, opRecords, request.headers, request.body, message);
|
||||
ctx.response.body = body;
|
||||
// headers 要拼上
|
||||
Object.keys(headers || {}).forEach(key => {
|
||||
ctx.set(key, headers?.[key]);
|
||||
});
|
||||
return;
|
||||
});
|
||||
// 桥接访问外部资源的入口
|
||||
|
|
@ -402,15 +436,63 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
|
|||
if (!omitTimers) {
|
||||
appLoader.startTimers();
|
||||
}
|
||||
process.on('SIGINT', async () => {
|
||||
await appLoader.unmount();
|
||||
process.exit(0);
|
||||
});
|
||||
let isShutingdown = false;
|
||||
const shutdown = async () => {
|
||||
await httpServer.close();
|
||||
await koa.removeAllListeners();
|
||||
await appLoader.unmount();
|
||||
(0, polyfill_1.removePolyfill)("startup");
|
||||
if (isShutingdown) {
|
||||
return;
|
||||
}
|
||||
isShutingdown = true;
|
||||
console.log('服务器正在关闭中...');
|
||||
try {
|
||||
await httpServer.close();
|
||||
await koa.removeAllListeners();
|
||||
await appLoader.execStopRoutines();
|
||||
await appLoader.unmount();
|
||||
}
|
||||
catch (err) {
|
||||
console.error('关闭服务器时出错:', err);
|
||||
}
|
||||
};
|
||||
// 处理优雅关闭的统一入口
|
||||
let shutdownStarted = false;
|
||||
const handleShutdown = async (signal) => {
|
||||
// 防止重复处理
|
||||
if (shutdownStarted) {
|
||||
console.warn('关闭流程已启动,忽略此信号');
|
||||
return;
|
||||
}
|
||||
shutdownStarted = true;
|
||||
console.log(`\n收到 ${signal} 信号,准备关闭服务器...`);
|
||||
// 移除所有信号处理器,防止重复触发
|
||||
process.removeAllListeners('SIGINT');
|
||||
process.removeAllListeners('SIGTERM');
|
||||
process.removeAllListeners('SIGQUIT');
|
||||
process.removeAllListeners('SIGHUP');
|
||||
try {
|
||||
await shutdown();
|
||||
process.exit(0);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('关闭过程出错:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
// 监听终止信号进行优雅关闭
|
||||
// SIGINT - Ctrl+C 发送的中断信号
|
||||
process.on('SIGINT', () => {
|
||||
handleShutdown('SIGINT');
|
||||
});
|
||||
// SIGTERM - 系统/容器管理器发送的终止信号(Docker、K8s、PM2等)
|
||||
process.on('SIGTERM', () => {
|
||||
handleShutdown('SIGTERM');
|
||||
});
|
||||
// SIGQUIT - Ctrl+\ 发送的退出信号
|
||||
process.on('SIGQUIT', () => {
|
||||
handleShutdown('SIGQUIT');
|
||||
});
|
||||
// SIGHUP - 终端关闭时发送(可选)
|
||||
process.on('SIGHUP', () => {
|
||||
handleShutdown('SIGHUP');
|
||||
});
|
||||
return shutdown;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
type TimerType = 'timeout' | 'interval' | 'immediate';
|
||||
interface TimerRecord {
|
||||
id: NodeJS.Timeout | NodeJS.Immediate;
|
||||
type: TimerType;
|
||||
createdAt: number;
|
||||
}
|
||||
declare class GlobalTimerManager {
|
||||
private timers;
|
||||
private isHooked;
|
||||
private readonly original;
|
||||
/**
|
||||
* 开始拦截全局定时器
|
||||
*/
|
||||
hook(): void;
|
||||
/**
|
||||
* 恢复原始的全局定时器函数
|
||||
*/
|
||||
unhook(): void;
|
||||
/**
|
||||
* 清除所有被追踪的定时器
|
||||
*/
|
||||
clearAll(): number;
|
||||
/**
|
||||
* 按类型清除定时器
|
||||
*/
|
||||
clearByType(type: TimerType): number;
|
||||
/**
|
||||
* 获取当前活跃的定时器数量
|
||||
*/
|
||||
getActiveCount(): number;
|
||||
/**
|
||||
* 获取定时器统计信息
|
||||
*/
|
||||
getStats(): Record<TimerType, number>;
|
||||
/**
|
||||
* 获取所有定时器的详细信息(用于调试)
|
||||
*/
|
||||
getTimers(): TimerRecord[];
|
||||
}
|
||||
export declare const timerManager: GlobalTimerManager;
|
||||
export declare const hookTimers: () => void;
|
||||
export declare const unhookTimers: () => void;
|
||||
export declare const clearAllTimers: () => number;
|
||||
export declare const getTimerStats: () => Record<TimerType, number>;
|
||||
export {};
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
"use strict";
|
||||
// timer-manager.ts
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getTimerStats = exports.clearAllTimers = exports.unhookTimers = exports.hookTimers = exports.timerManager = void 0;
|
||||
class GlobalTimerManager {
|
||||
timers = new Map();
|
||||
isHooked = false;
|
||||
// 保存原始函数
|
||||
original = {
|
||||
setTimeout: global.setTimeout,
|
||||
setInterval: global.setInterval,
|
||||
setImmediate: global.setImmediate, clearTimeout: global.clearTimeout,
|
||||
clearInterval: global.clearInterval,
|
||||
clearImmediate: global.clearImmediate,
|
||||
};
|
||||
/**
|
||||
* 开始拦截全局定时器
|
||||
*/
|
||||
hook() {
|
||||
if (this.isHooked) {
|
||||
console.warn('[TimerManager] Already hooked');
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
// Hook setTimeout
|
||||
global.setTimeout = function (callback, ms, ...args) {
|
||||
const id = self.original.setTimeout((...callbackArgs) => {
|
||||
self.timers.delete(id); // 执行完后移除
|
||||
callback(...callbackArgs);
|
||||
}, ms, ...args);
|
||||
self.timers.set(id, {
|
||||
id,
|
||||
type: 'timeout',
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
return id;
|
||||
};
|
||||
// Hook setInterval
|
||||
global.setInterval = function (callback, ms, ...args) {
|
||||
const id = self.original.setInterval(callback, ms, ...args);
|
||||
self.timers.set(id, {
|
||||
id,
|
||||
type: 'interval',
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
return id;
|
||||
};
|
||||
// Hook setImmediate
|
||||
global.setImmediate = function (callback, ...args) {
|
||||
const id = self.original.setImmediate((...callbackArgs) => {
|
||||
self.timers.delete(id); // 执行完后移除
|
||||
callback(...callbackArgs);
|
||||
}, ...args);
|
||||
self.timers.set(id, {
|
||||
id,
|
||||
type: 'immediate',
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
return id;
|
||||
};
|
||||
// Hook clear方法(确保从追踪中移除)
|
||||
global.clearTimeout = ((id) => {
|
||||
self.timers.delete(id);
|
||||
return self.original.clearTimeout(id);
|
||||
});
|
||||
global.clearInterval = ((id) => {
|
||||
self.timers.delete(id);
|
||||
return self.original.clearInterval(id);
|
||||
});
|
||||
global.clearImmediate = ((id) => {
|
||||
self.timers.delete(id);
|
||||
return self.original.clearImmediate(id);
|
||||
});
|
||||
this.isHooked = true;
|
||||
console.log('[TimerManager] Hooked global timer functions');
|
||||
}
|
||||
/**
|
||||
* 恢复原始的全局定时器函数
|
||||
*/
|
||||
unhook() {
|
||||
if (!this.isHooked) {
|
||||
return;
|
||||
}
|
||||
global.setTimeout = this.original.setTimeout;
|
||||
global.setInterval = this.original.setInterval;
|
||||
global.setImmediate = this.original.setImmediate;
|
||||
global.clearTimeout = this.original.clearTimeout;
|
||||
global.clearInterval = this.original.clearInterval;
|
||||
global.clearImmediate = this.original.clearImmediate;
|
||||
this.isHooked = false;
|
||||
}
|
||||
/**
|
||||
* 清除所有被追踪的定时器
|
||||
*/
|
||||
clearAll() {
|
||||
const count = this.timers.size;
|
||||
this.timers.forEach((record) => {
|
||||
if (record.type === 'immediate') {
|
||||
this.original.clearImmediate.call(null, record.id);
|
||||
}
|
||||
else {
|
||||
this.original.clearTimeout.call(null, record.id);
|
||||
}
|
||||
});
|
||||
this.timers.clear();
|
||||
return count;
|
||||
}
|
||||
/**
|
||||
* 按类型清除定时器
|
||||
*/
|
||||
clearByType(type) {
|
||||
let count = 0;
|
||||
this.timers.forEach((record, id) => {
|
||||
if (record.type === type) {
|
||||
if (type === 'immediate') {
|
||||
this.original.clearImmediate.call(null, id);
|
||||
}
|
||||
else {
|
||||
this.original.clearTimeout.call(null, id);
|
||||
}
|
||||
this.timers.delete(id);
|
||||
count++;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}
|
||||
/**
|
||||
* 获取当前活跃的定时器数量
|
||||
*/
|
||||
getActiveCount() {
|
||||
return this.timers.size;
|
||||
}
|
||||
/**
|
||||
* 获取定时器统计信息
|
||||
*/
|
||||
getStats() {
|
||||
const stats = {
|
||||
timeout: 0,
|
||||
interval: 0,
|
||||
immediate: 0,
|
||||
};
|
||||
this.timers.forEach((record) => {
|
||||
stats[record.type]++;
|
||||
});
|
||||
return stats;
|
||||
}
|
||||
/**
|
||||
* 获取所有定时器的详细信息(用于调试)
|
||||
*/
|
||||
getTimers() {
|
||||
return Array.from(this.timers.values());
|
||||
}
|
||||
}
|
||||
exports.timerManager = new GlobalTimerManager();
|
||||
const hookTimers = () => exports.timerManager.hook();
|
||||
exports.hookTimers = hookTimers;
|
||||
const unhookTimers = () => exports.timerManager.unhook();
|
||||
exports.unhookTimers = unhookTimers;
|
||||
const clearAllTimers = () => exports.timerManager.clearAll();
|
||||
exports.clearAllTimers = clearAllTimers;
|
||||
const getTimerStats = () => exports.timerManager.getStats();
|
||||
exports.getTimerStats = getTimerStats;
|
||||
|
|
@ -9,6 +9,7 @@ const dayjs_1 = tslib_1.__importDefault(require("dayjs"));
|
|||
const fs_1 = tslib_1.__importDefault(require("fs"));
|
||||
const polyfill_1 = require("./polyfill");
|
||||
const tsc_alias_1 = require("tsc-alias");
|
||||
const timer_manager_1 = require("./timer-manager");
|
||||
// 创建事件发射器
|
||||
const createEventEmitter = () => {
|
||||
const listeners = new Map();
|
||||
|
|
@ -228,6 +229,7 @@ const createServerManager = (projectPath, eventEmitter, config) => {
|
|||
const simpleConnector = require(path_1.default.join(projectPath, "lib/config/connector")).default;
|
||||
console.warn("----> Starting service......");
|
||||
try {
|
||||
(0, timer_manager_1.clearAllTimers)();
|
||||
// 这里注意要在require之前,因为require会触发编译
|
||||
const { startup } = require('./start');
|
||||
shutdown = await startup(pwd, simpleConnector).then((shutdown) => {
|
||||
|
|
@ -280,11 +282,12 @@ const createCompiler = async (projectPath, options, projectReferences, treatFile
|
|||
};
|
||||
const compileTask = async (task) => {
|
||||
const { filePath, changeType } = task;
|
||||
const modulePath = path_1.default.resolve(filePath);
|
||||
// 判断文件类型
|
||||
if (!filePath.endsWith(".ts")) {
|
||||
// 处理非TypeScript文件 (如JSON文件)
|
||||
if (filePath.endsWith(".json")) {
|
||||
const targetPath = filePath.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib"));
|
||||
const targetPath = modulePath.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib"));
|
||||
try {
|
||||
if (changeType === "remove") {
|
||||
if (fs_1.default.existsSync(targetPath)) {
|
||||
|
|
@ -331,7 +334,6 @@ const createCompiler = async (projectPath, options, projectReferences, treatFile
|
|||
// 处理TypeScript文件
|
||||
console.clear();
|
||||
console.warn(`File ${filePath} has been ${changeType}d`);
|
||||
const modulePath = path_1.default.resolve(filePath);
|
||||
const libPath = modulePath
|
||||
.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib"))
|
||||
.replace(/\.ts$/, ".js");
|
||||
|
|
@ -499,6 +501,7 @@ const watch = async (projectPath, config) => {
|
|||
resolveFullPaths: true,
|
||||
});
|
||||
function treatFile(filePath) {
|
||||
console.log(`Processing file for alias replacement: ${filePath}`);
|
||||
const fileContents = fs_1.default.readFileSync(filePath, 'utf8');
|
||||
const newContents = runFile({ fileContents, filePath });
|
||||
// do stuff with newContents
|
||||
|
|
@ -605,6 +608,8 @@ const watch = async (projectPath, config) => {
|
|||
// 初始化polyfill
|
||||
polyfillLoader();
|
||||
realConfig.polyfill.console.enable && (0, polyfill_1.polyfillConsole)("watch", enableTrace, realConfig.polyfill?.console?.formatter);
|
||||
// 监听定时器相关的API,确保定时器在重启后不会重复触发
|
||||
(0, timer_manager_1.hookTimers)();
|
||||
// 初始编译检查
|
||||
const initialCompile = () => {
|
||||
const serverConfigFile = path_1.default.join(projectPath, "lib/configuration/server.js");
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i
|
|||
"build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze",
|
||||
"build:mp": "${cliBinName} build --target mp --mode production",
|
||||
"build-analyze:mp": "${cliBinName} build --target mp --mode production --analyze",
|
||||
"build:watch": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json && npm run server:start:watch",
|
||||
"build:watch": "node --stack-size=4096 ./scripts/build.js -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json && npm run server:start:watch",
|
||||
"start:web": "${cliBinName} start --target web --mode development --devMode frontend",
|
||||
"start:web:server": "${cliBinName} start --target web --mode development",
|
||||
"start:native": "${cliBinName} start --target rn --mode development --devMode frontend",
|
||||
|
|
@ -97,7 +97,7 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i
|
|||
"build-sourcemap:web": "${cliBinName} build --target web --mode production --sourcemap",
|
||||
"build-analyze:web": "${cliBinName} build --target web --mode production --analyze",
|
||||
"build-sourcemap-analyze:web": "${cliBinName} build --target web --mode production --sourcemap --analyze",
|
||||
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json",
|
||||
"build": "node --stack-size=4096 ./scripts/build.js -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json",
|
||||
"prebuild": "npm run make:locale",
|
||||
"run:ios": "oak-cli run -p ios",
|
||||
"run:android": "oak-cli run -p android",
|
||||
|
|
@ -397,7 +397,13 @@ function tsConfigBuildJsonContent() {
|
|||
"test",
|
||||
"src/pages/**/*",
|
||||
"src/components/**/*"
|
||||
]
|
||||
],
|
||||
"oakBuildChecks": {
|
||||
"context": {
|
||||
"checkAsyncContext": true,
|
||||
"targetModules": ["context/BackendRuntimeContext"]
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
function tsConfigPathsJsonContent(deps) {
|
||||
|
|
@ -424,7 +430,7 @@ function tsConfigPathsJsonContent(deps) {
|
|||
compilerOptions: {
|
||||
baseUrl: "./",
|
||||
paths,
|
||||
typeRoots: ["./typings"]
|
||||
typeRoots: ["./typings", "node_modules/@types"]
|
||||
}
|
||||
}, null, '\t');
|
||||
}
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@xuchangzju/oak-cli",
|
||||
"version": "4.0.29",
|
||||
"version": "4.0.33",
|
||||
"description": "client for oak framework",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"events": "^3.3.0",
|
||||
"fork-ts-checker-webpack-plugin": "^8.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -112,9 +112,9 @@
|
|||
"lodash": "^4.17.21",
|
||||
"mini-css-extract-plugin": "^2.5.3",
|
||||
"node-watch": "^0.7.4",
|
||||
"oak-backend-base": "^4.1.24",
|
||||
"oak-domain": "^5.1.30",
|
||||
"oak-frontend-base": "^5.3.43",
|
||||
"oak-backend-base": "file:../oak-backend-base",
|
||||
"oak-domain": "file:../oak-domain",
|
||||
"oak-frontend-base": "file:../oak-frontend-base",
|
||||
"parse-asn1": "5.1.6",
|
||||
"postcss": "^8.4.4",
|
||||
"postcss-flexbugs-fixes": "^5.0.2",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import KoaBody from 'koa-body';
|
|||
|
||||
import { AppLoader, getClusterInfo, ClusterAppLoader } from 'oak-backend-base';
|
||||
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
|
||||
import { OakException, Connector, EntityDict, ClusterInfo } from 'oak-domain/lib/types';
|
||||
import { OakException, Connector, EntityDict, ClusterInfo, OpRecord, isOakException } from 'oak-domain/lib/types';
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
||||
import { AsyncRowStore, AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
|
||||
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
|
||||
|
|
@ -25,7 +25,6 @@ import mount from 'koa-mount';
|
|||
import chalk from 'chalk';
|
||||
import { checkNodeVersion, randomString } from '../utils';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { LogFormatter, polyfillConsole, removePolyfill } from './polyfill';
|
||||
|
||||
checkNodeVersion()
|
||||
|
||||
|
|
@ -216,7 +215,7 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
|
|||
if (routine) {
|
||||
// 如果传入了routine,执行完成后就结束
|
||||
const result = await appLoader.execRoutine(routine);
|
||||
await appLoader.unmount();
|
||||
// await appLoader.unmount(); // 不卸载,在进程退出时会自动卸载
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -245,26 +244,32 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
|
|||
console.error(err);
|
||||
const { request } = ctx;
|
||||
const exception =
|
||||
err instanceof OakException
|
||||
isOakException(err)
|
||||
? err
|
||||
: new OakException<ED>(
|
||||
serverConfiguration?.internalExceptionMask ||
|
||||
ExceptionMask
|
||||
);
|
||||
const { body } = connector.serializeException(
|
||||
const { body, headers } = connector.serializeException(
|
||||
exception,
|
||||
request.headers,
|
||||
request.body
|
||||
);
|
||||
ctx.response.body = body;
|
||||
// headers 要拼上
|
||||
Object.keys(headers || {}).forEach(key => {
|
||||
ctx.set(key, headers?.[key])
|
||||
})
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
koa.use(
|
||||
KoaBody(Object.assign({
|
||||
multipart: true,
|
||||
}, serverConfiguration.koaBody))
|
||||
);
|
||||
|
||||
// 注册自定义中间件
|
||||
if (serverConfiguration.middleware) {
|
||||
if (Array.isArray(serverConfiguration.middleware)) {
|
||||
|
|
@ -290,6 +295,10 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
|
|||
ctx.set('Access-Control-Allow-Origin', '*');
|
||||
ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader()));
|
||||
ctx.set('Access-Control-Allow-Methods', corsMethods);
|
||||
if (connector.getCorsExposeHeaders) {
|
||||
const exposeHeaders = connector.getCorsExposeHeaders();
|
||||
ctx.set('Access-Control-Expose-Headers', exposeHeaders);
|
||||
}
|
||||
if (ctx.method == 'OPTIONS') {
|
||||
ctx.body = 200;
|
||||
} else {
|
||||
|
|
@ -328,20 +337,48 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
|
|||
});
|
||||
}
|
||||
|
||||
const connectorCustomAspects = connector.registerCustomAspect ? connector.registerCustomAspect() : null;
|
||||
|
||||
router.post(connector.getRouter(), async (ctx) => {
|
||||
const { request } = ctx;
|
||||
const { contextString, aspectName, data } = connector.parseRequest(
|
||||
const { contextString, aspectName, data } = await connector.parseRequest(
|
||||
request.headers,
|
||||
request.body,
|
||||
request.files
|
||||
);
|
||||
|
||||
const { result, opRecords, message } = await appLoader.execAspect(
|
||||
aspectName,
|
||||
request.headers,
|
||||
contextString,
|
||||
data
|
||||
);
|
||||
let result: any;
|
||||
let opRecords: OpRecord<ED>[] = [];
|
||||
let message: string | undefined = undefined;
|
||||
|
||||
if (connectorCustomAspects &&
|
||||
connectorCustomAspects.findIndex(a => a.name === aspectName) >= 0
|
||||
) {
|
||||
// 自定义aspect处理
|
||||
console.log(`调用Connector自定义Aspect: ${aspectName}`);
|
||||
const aspect = connectorCustomAspects!.find(a => a.name === aspectName)!;
|
||||
const res = await aspect.handler(
|
||||
{
|
||||
headers: request.headers,
|
||||
contextString,
|
||||
params: data,
|
||||
}
|
||||
);
|
||||
result = res.result;
|
||||
opRecords = res.opRecords || [];
|
||||
message = res.message;
|
||||
} else {
|
||||
const res = await appLoader.execAspect(
|
||||
aspectName,
|
||||
request.headers,
|
||||
contextString,
|
||||
data
|
||||
);
|
||||
result = res.result;
|
||||
opRecords = res.opRecords || [];
|
||||
message = res.message;
|
||||
}
|
||||
|
||||
const { body, headers } = await connector.serializeResult(
|
||||
result,
|
||||
opRecords,
|
||||
|
|
@ -350,6 +387,12 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
|
|||
message
|
||||
);
|
||||
ctx.response.body = body;
|
||||
|
||||
// headers 要拼上
|
||||
Object.keys(headers || {}).forEach(key => {
|
||||
ctx.set(key, headers?.[key])
|
||||
})
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
|
|
@ -492,18 +535,70 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
|
|||
appLoader.startTimers();
|
||||
}
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await appLoader.unmount();
|
||||
process.exit(0);
|
||||
let isShutingdown = false;
|
||||
const shutdown = async () => {
|
||||
if (isShutingdown) {
|
||||
return;
|
||||
}
|
||||
isShutingdown = true;
|
||||
console.log('服务器正在关闭中...');
|
||||
try {
|
||||
await httpServer.close();
|
||||
await koa.removeAllListeners();
|
||||
await appLoader.execStopRoutines();
|
||||
await appLoader.unmount();
|
||||
} catch (err) {
|
||||
console.error('关闭服务器时出错:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理优雅关闭的统一入口
|
||||
let shutdownStarted = false;
|
||||
const handleShutdown = async (signal: string) => {
|
||||
// 防止重复处理
|
||||
if (shutdownStarted) {
|
||||
console.warn('关闭流程已启动,忽略此信号');
|
||||
return;
|
||||
}
|
||||
shutdownStarted = true;
|
||||
|
||||
console.log(`\n收到 ${signal} 信号,准备关闭服务器...`);
|
||||
|
||||
// 移除所有信号处理器,防止重复触发
|
||||
process.removeAllListeners('SIGINT');
|
||||
process.removeAllListeners('SIGTERM');
|
||||
process.removeAllListeners('SIGQUIT');
|
||||
process.removeAllListeners('SIGHUP');
|
||||
|
||||
try {
|
||||
await shutdown();
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('关闭过程出错:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听终止信号进行优雅关闭
|
||||
// SIGINT - Ctrl+C 发送的中断信号
|
||||
process.on('SIGINT', () => {
|
||||
handleShutdown('SIGINT');
|
||||
});
|
||||
|
||||
const shutdown = async () => {
|
||||
await httpServer.close();
|
||||
await koa.removeAllListeners();
|
||||
await appLoader.unmount();
|
||||
// SIGTERM - 系统/容器管理器发送的终止信号(Docker、K8s、PM2等)
|
||||
process.on('SIGTERM', () => {
|
||||
handleShutdown('SIGTERM');
|
||||
});
|
||||
|
||||
removePolyfill("startup");
|
||||
}
|
||||
// SIGQUIT - Ctrl+\ 发送的退出信号
|
||||
process.on('SIGQUIT', () => {
|
||||
handleShutdown('SIGQUIT');
|
||||
});
|
||||
|
||||
// SIGHUP - 终端关闭时发送(可选)
|
||||
process.on('SIGHUP', () => {
|
||||
handleShutdown('SIGHUP');
|
||||
});
|
||||
|
||||
return shutdown
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
// timer-manager.ts
|
||||
|
||||
type TimerType = 'timeout' | 'interval' | 'immediate';
|
||||
|
||||
interface TimerRecord {
|
||||
id: NodeJS.Timeout | NodeJS.Immediate;
|
||||
type: TimerType;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
class GlobalTimerManager {
|
||||
private timers = new Map<NodeJS.Timeout | NodeJS.Immediate, TimerRecord>();
|
||||
private isHooked = false;
|
||||
|
||||
// 保存原始函数
|
||||
private readonly original = {
|
||||
setTimeout: global.setTimeout,
|
||||
setInterval: global.setInterval,
|
||||
setImmediate: global.setImmediate,clearTimeout: global.clearTimeout,
|
||||
clearInterval: global.clearInterval,
|
||||
clearImmediate: global.clearImmediate,};
|
||||
|
||||
/**
|
||||
* 开始拦截全局定时器
|
||||
*/
|
||||
hook(): void {
|
||||
if (this.isHooked) {
|
||||
console.warn('[TimerManager] Already hooked');
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
// Hook setTimeout
|
||||
global.setTimeout = function (
|
||||
callback: (...args: any[]) => void,
|
||||
ms?: number,
|
||||
...args: any[]
|
||||
): any {
|
||||
const id = self.original.setTimeout(
|
||||
(...callbackArgs: any[]) => {
|
||||
self.timers.delete(id); // 执行完后移除
|
||||
callback(...callbackArgs);
|
||||
},
|
||||
ms,
|
||||
...args
|
||||
);
|
||||
|
||||
self.timers.set(id, {
|
||||
id,
|
||||
type: 'timeout',
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
|
||||
return id;
|
||||
} as any;
|
||||
|
||||
// Hook setInterval
|
||||
global.setInterval = function (
|
||||
callback: (...args: any[]) => void,
|
||||
ms?: number,
|
||||
...args: any[]
|
||||
): any {
|
||||
const id = self.original.setInterval(callback, ms, ...args);
|
||||
|
||||
self.timers.set(id, {
|
||||
id,
|
||||
type: 'interval',
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
|
||||
return id;
|
||||
} as any;
|
||||
|
||||
// Hook setImmediate
|
||||
global.setImmediate = function (
|
||||
callback: (...args: any[]) => void,
|
||||
...args: any[]
|
||||
): any {
|
||||
const id = self.original.setImmediate(
|
||||
(...callbackArgs: any[]) => {
|
||||
self.timers.delete(id); // 执行完后移除
|
||||
callback(...callbackArgs);
|
||||
},
|
||||
...args
|
||||
);
|
||||
|
||||
self.timers.set(id, {
|
||||
id,
|
||||
type: 'immediate',
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
|
||||
return id;
|
||||
} as any;
|
||||
|
||||
// Hook clear方法(确保从追踪中移除)
|
||||
global.clearTimeout = ((id: any) => {
|
||||
self.timers.delete(id);
|
||||
return self.original.clearTimeout(id);
|
||||
}) as any;
|
||||
|
||||
global.clearInterval = ((id: any) => {
|
||||
self.timers.delete(id);
|
||||
return self.original.clearInterval(id);
|
||||
}) as any;
|
||||
|
||||
global.clearImmediate = ((id: any) => {
|
||||
self.timers.delete(id);
|
||||
return self.original.clearImmediate(id);
|
||||
}) as any;
|
||||
|
||||
this.isHooked = true;
|
||||
console.log('[TimerManager] Hooked global timer functions');
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复原始的全局定时器函数
|
||||
*/
|
||||
unhook(): void {
|
||||
if (!this.isHooked) {
|
||||
return;
|
||||
}
|
||||
|
||||
global.setTimeout = this.original.setTimeout;
|
||||
global.setInterval = this.original.setInterval;
|
||||
global.setImmediate = this.original.setImmediate;
|
||||
global.clearTimeout = this.original.clearTimeout;
|
||||
global.clearInterval = this.original.clearInterval;
|
||||
global.clearImmediate = this.original.clearImmediate;
|
||||
|
||||
this.isHooked = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有被追踪的定时器
|
||||
*/
|
||||
clearAll(): number {
|
||||
const count = this.timers.size;
|
||||
|
||||
this.timers.forEach((record) => {
|
||||
if (record.type === 'immediate') {
|
||||
this.original.clearImmediate.call(null, record.id as NodeJS.Immediate);
|
||||
} else {
|
||||
this.original.clearTimeout.call(null, record.id as NodeJS.Timeout);
|
||||
}
|
||||
});
|
||||
|
||||
this.timers.clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按类型清除定时器
|
||||
*/
|
||||
clearByType(type: TimerType): number {
|
||||
let count = 0;
|
||||
|
||||
this.timers.forEach((record, id) => {
|
||||
if (record.type === type) {
|
||||
if (type === 'immediate') {
|
||||
this.original.clearImmediate.call(null, id as NodeJS.Immediate);
|
||||
} else {
|
||||
this.original.clearTimeout.call(null, id as NodeJS.Timeout);
|
||||
}
|
||||
this.timers.delete(id);
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前活跃的定时器数量
|
||||
*/
|
||||
getActiveCount(): number {
|
||||
return this.timers.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取定时器统计信息
|
||||
*/
|
||||
getStats(): Record<TimerType, number> {
|
||||
const stats: Record<TimerType, number> = {
|
||||
timeout: 0,
|
||||
interval: 0,
|
||||
immediate: 0,
|
||||
};
|
||||
|
||||
this.timers.forEach((record) => {
|
||||
stats[record.type]++;
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有定时器的详细信息(用于调试)
|
||||
*/
|
||||
getTimers(): TimerRecord[] {
|
||||
return Array.from(this.timers.values());
|
||||
}
|
||||
}
|
||||
|
||||
export const timerManager = new GlobalTimerManager();
|
||||
|
||||
export const hookTimers = () => timerManager.hook();
|
||||
export const unhookTimers = () => timerManager.unhook();
|
||||
export const clearAllTimers = () => timerManager.clearAll();
|
||||
export const getTimerStats = () => timerManager.getStats();
|
||||
|
|
@ -7,6 +7,7 @@ import fs from "fs";
|
|||
import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore";
|
||||
import { LogFormatter, LogFormatterProp, polyfillConsole } from "./polyfill";
|
||||
import { prepareSingleFileReplaceTscAliasPaths, SingleFileReplacer } from 'tsc-alias';
|
||||
import { clearAllTimers, hookTimers } from "./timer-manager";
|
||||
|
||||
/*
|
||||
* 工作流程
|
||||
|
|
@ -399,6 +400,7 @@ const createServerManager = (
|
|||
console.warn("----> Starting service......");
|
||||
|
||||
try {
|
||||
clearAllTimers();
|
||||
// 这里注意要在require之前,因为require会触发编译
|
||||
const { startup } = require('./start') as {
|
||||
startup: (pwd: string, connector: any) => Promise<() => Promise<void>>;
|
||||
|
|
@ -477,11 +479,12 @@ const createCompiler = async (
|
|||
const compileTask = async (task: CompileTask): Promise<CompileResult> => {
|
||||
const { filePath, changeType } = task;
|
||||
|
||||
const modulePath = pathLib.resolve(filePath);
|
||||
// 判断文件类型
|
||||
if (!filePath.endsWith(".ts")) {
|
||||
// 处理非TypeScript文件 (如JSON文件)
|
||||
if (filePath.endsWith(".json")) {
|
||||
const targetPath = filePath.replace(
|
||||
const targetPath = modulePath.replace(
|
||||
pathLib.join(projectPath, "src"),
|
||||
pathLib.join(projectPath, "lib")
|
||||
);
|
||||
|
|
@ -533,7 +536,6 @@ const createCompiler = async (
|
|||
console.clear();
|
||||
console.warn(`File ${filePath} has been ${changeType}d`);
|
||||
|
||||
const modulePath = pathLib.resolve(filePath);
|
||||
const libPath = modulePath
|
||||
.replace(pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib"))
|
||||
.replace(/\.ts$/, ".js");
|
||||
|
|
@ -879,6 +881,9 @@ export const watch = async (
|
|||
polyfillLoader();
|
||||
realConfig.polyfill.console.enable && polyfillConsole("watch", enableTrace, realConfig.polyfill?.console?.formatter);
|
||||
|
||||
// 监听定时器相关的API,确保定时器在重启后不会重复触发
|
||||
hookTimers()
|
||||
|
||||
// 初始编译检查
|
||||
const initialCompile = () => {
|
||||
const serverConfigFile = pathLib.join(projectPath, "lib/configuration/server.js");
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export function packageJsonContent({
|
|||
"build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze",
|
||||
"build:mp": "${cliBinName} build --target mp --mode production",
|
||||
"build-analyze:mp": "${cliBinName} build --target mp --mode production --analyze",
|
||||
"build:watch": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json && npm run server:start:watch",
|
||||
"build:watch": "node --stack-size=4096 ./scripts/build.js -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json && npm run server:start:watch",
|
||||
"start:web": "${cliBinName} start --target web --mode development --devMode frontend",
|
||||
"start:web:server": "${cliBinName} start --target web --mode development",
|
||||
"start:native": "${cliBinName} start --target rn --mode development --devMode frontend",
|
||||
|
|
@ -104,7 +104,7 @@ export function packageJsonContent({
|
|||
"build-sourcemap:web": "${cliBinName} build --target web --mode production --sourcemap",
|
||||
"build-analyze:web": "${cliBinName} build --target web --mode production --analyze",
|
||||
"build-sourcemap-analyze:web": "${cliBinName} build --target web --mode production --sourcemap --analyze",
|
||||
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json",
|
||||
"build": "node --stack-size=4096 ./scripts/build.js -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run copy-config-json",
|
||||
"prebuild": "npm run make:locale",
|
||||
"run:ios": "oak-cli run -p ios",
|
||||
"run:android": "oak-cli run -p android",
|
||||
|
|
@ -406,7 +406,13 @@ export function tsConfigBuildJsonContent() {
|
|||
"test",
|
||||
"src/pages/**/*",
|
||||
"src/components/**/*"
|
||||
]
|
||||
],
|
||||
"oakBuildChecks": {
|
||||
"context": {
|
||||
"checkAsyncContext": true,
|
||||
"targetModules": ["context/BackendRuntimeContext"]
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { build } = require('oak-domain/lib/compiler/tscBuilder.js')
|
||||
|
||||
const pwd = process.cwd();
|
||||
|
||||
build(pwd, process.argv);
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { Routine } from 'oak-domain/lib/types/Timer';
|
||||
import { EntityDict } from '@oak-app-domain';
|
||||
import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
|
||||
|
||||
// process.on('uncaughtException', (err) => {
|
||||
// console.error(`Caught exception: ${err}`);
|
||||
// // Optionally, you can exit the process or perform cleanup
|
||||
// });
|
||||
|
||||
// process.on('unhandledRejection', (err) => {
|
||||
// console.error(`Caught rejection: ${err}`);
|
||||
// // Optionally, you can exit the process or perform cleanup
|
||||
// });
|
||||
|
||||
|
||||
const stopRoutines: Array<Routine<EntityDict, keyof EntityDict, BackendRuntimeContext>> = [
|
||||
{
|
||||
name: '示例性routine_stop',
|
||||
routine: async (context, env) => {
|
||||
console.log('示例性routine执行,请在src/routine/stop.ts中关闭');
|
||||
return context.opResult;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default stopRoutines;
|
||||
Loading…
Reference in New Issue