Compare commits

..

13 Commits
4.0.30 ... dev

11 changed files with 557 additions and 37 deletions

View File

@ -23,7 +23,6 @@ const koa_mount_1 = tslib_1.__importDefault(require("koa-mount"));
const chalk_1 = tslib_1.__importDefault(require("chalk")); const chalk_1 = tslib_1.__importDefault(require("chalk"));
const utils_1 = require("../utils"); const utils_1 = require("../utils");
const bcryptjs_1 = tslib_1.__importDefault(require("bcryptjs")); const bcryptjs_1 = tslib_1.__importDefault(require("bcryptjs"));
const polyfill_1 = require("./polyfill");
(0, utils_1.checkNodeVersion)(); (0, utils_1.checkNodeVersion)();
const socketAdminUI = (0, path_1.join)(__dirname, '../../ui/socket-admin'); const socketAdminUI = (0, path_1.join)(__dirname, '../../ui/socket-admin');
const DATA_SUBSCRIBE_NAMESPACE = '/dsn'; const DATA_SUBSCRIBE_NAMESPACE = '/dsn';
@ -173,7 +172,7 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
if (routine) { if (routine) {
// 如果传入了routine执行完成后就结束 // 如果传入了routine执行完成后就结束
const result = await appLoader.execRoutine(routine); const result = await appLoader.execRoutine(routine);
await appLoader.unmount(); // await appLoader.unmount(); // 不卸载,在进程退出时会自动卸载
return result; return result;
} }
// if (errorHandler && typeof errorHandler === 'function') { // if (errorHandler && typeof errorHandler === 'function') {
@ -200,12 +199,16 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
catch (err) { catch (err) {
console.error(err); console.error(err);
const { request } = ctx; const { request } = ctx;
const exception = err instanceof types_1.OakException const exception = (0, types_1.isOakException)(err)
? err ? err
: new types_1.OakException(serverConfiguration?.internalExceptionMask || : new types_1.OakException(serverConfiguration?.internalExceptionMask ||
ExceptionMask); ExceptionMask);
const { body } = connector.serializeException(exception, request.headers, request.body); const { body, headers } = connector.serializeException(exception, request.headers, request.body);
ctx.response.body = body; ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key]);
});
return; return;
} }
}); });
@ -236,6 +239,10 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
ctx.set('Access-Control-Allow-Origin', '*'); ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader())); ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader()));
ctx.set('Access-Control-Allow-Methods', corsMethods); 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') { if (ctx.method == 'OPTIONS') {
ctx.body = 200; 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) => { router.post(connector.getRouter(), async (ctx) => {
const { request } = ctx; const { request } = ctx;
const { contextString, aspectName, data } = connector.parseRequest(request.headers, request.body, request.files); 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;
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); const { body, headers } = await connector.serializeResult(result, opRecords, request.headers, request.body, message);
ctx.response.body = body; ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key]);
});
return; return;
}); });
// 桥接访问外部资源的入口 // 桥接访问外部资源的入口
@ -414,7 +448,6 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) {
await koa.removeAllListeners(); await koa.removeAllListeners();
await appLoader.execStopRoutines(); await appLoader.execStopRoutines();
await appLoader.unmount(); await appLoader.unmount();
(0, polyfill_1.removePolyfill)("startup");
} }
catch (err) { catch (err) {
console.error('关闭服务器时出错:', err); console.error('关闭服务器时出错:', err);

45
lib/server/timer-manager.d.ts vendored Normal file
View File

@ -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 {};

162
lib/server/timer-manager.js Normal file
View File

@ -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;

View File

@ -9,6 +9,7 @@ const dayjs_1 = tslib_1.__importDefault(require("dayjs"));
const fs_1 = tslib_1.__importDefault(require("fs")); const fs_1 = tslib_1.__importDefault(require("fs"));
const polyfill_1 = require("./polyfill"); const polyfill_1 = require("./polyfill");
const tsc_alias_1 = require("tsc-alias"); const tsc_alias_1 = require("tsc-alias");
const timer_manager_1 = require("./timer-manager");
// 创建事件发射器 // 创建事件发射器
const createEventEmitter = () => { const createEventEmitter = () => {
const listeners = new Map(); 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; const simpleConnector = require(path_1.default.join(projectPath, "lib/config/connector")).default;
console.warn("----> Starting service......"); console.warn("----> Starting service......");
try { try {
(0, timer_manager_1.clearAllTimers)();
// 这里注意要在require之前因为require会触发编译 // 这里注意要在require之前因为require会触发编译
const { startup } = require('./start'); const { startup } = require('./start');
shutdown = await startup(pwd, simpleConnector).then((shutdown) => { shutdown = await startup(pwd, simpleConnector).then((shutdown) => {
@ -280,11 +282,12 @@ const createCompiler = async (projectPath, options, projectReferences, treatFile
}; };
const compileTask = async (task) => { const compileTask = async (task) => {
const { filePath, changeType } = task; const { filePath, changeType } = task;
const modulePath = path_1.default.resolve(filePath);
// 判断文件类型 // 判断文件类型
if (!filePath.endsWith(".ts")) { if (!filePath.endsWith(".ts")) {
// 处理非TypeScript文件 (如JSON文件) // 处理非TypeScript文件 (如JSON文件)
if (filePath.endsWith(".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 { try {
if (changeType === "remove") { if (changeType === "remove") {
if (fs_1.default.existsSync(targetPath)) { if (fs_1.default.existsSync(targetPath)) {
@ -331,7 +334,6 @@ const createCompiler = async (projectPath, options, projectReferences, treatFile
// 处理TypeScript文件 // 处理TypeScript文件
console.clear(); console.clear();
console.warn(`File ${filePath} has been ${changeType}d`); console.warn(`File ${filePath} has been ${changeType}d`);
const modulePath = path_1.default.resolve(filePath);
const libPath = modulePath const libPath = modulePath
.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib")) .replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib"))
.replace(/\.ts$/, ".js"); .replace(/\.ts$/, ".js");
@ -606,6 +608,8 @@ const watch = async (projectPath, config) => {
// 初始化polyfill // 初始化polyfill
polyfillLoader(); polyfillLoader();
realConfig.polyfill.console.enable && (0, polyfill_1.polyfillConsole)("watch", enableTrace, realConfig.polyfill?.console?.formatter); realConfig.polyfill.console.enable && (0, polyfill_1.polyfillConsole)("watch", enableTrace, realConfig.polyfill?.console?.formatter);
// 监听定时器相关的API,确保定时器在重启后不会重复触发
(0, timer_manager_1.hookTimers)();
// 初始编译检查 // 初始编译检查
const initialCompile = () => { const initialCompile = () => {
const serverConfigFile = path_1.default.join(projectPath, "lib/configuration/server.js"); const serverConfigFile = path_1.default.join(projectPath, "lib/configuration/server.js");

View File

@ -85,7 +85,7 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i
"build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze", "build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze",
"build:mp": "${cliBinName} build --target mp --mode production", "build:mp": "${cliBinName} build --target mp --mode production",
"build-analyze:mp": "${cliBinName} build --target mp --mode production --analyze", "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": "${cliBinName} start --target web --mode development --devMode frontend",
"start:web:server": "${cliBinName} start --target web --mode development", "start:web:server": "${cliBinName} start --target web --mode development",
"start:native": "${cliBinName} start --target rn --mode development --devMode frontend", "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-sourcemap:web": "${cliBinName} build --target web --mode production --sourcemap",
"build-analyze:web": "${cliBinName} build --target web --mode production --analyze", "build-analyze:web": "${cliBinName} build --target web --mode production --analyze",
"build-sourcemap-analyze:web": "${cliBinName} build --target web --mode production --sourcemap --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", "prebuild": "npm run make:locale",
"run:ios": "oak-cli run -p ios", "run:ios": "oak-cli run -p ios",
"run:android": "oak-cli run -p android", "run:android": "oak-cli run -p android",
@ -397,7 +397,13 @@ function tsConfigBuildJsonContent() {
"test", "test",
"src/pages/**/*", "src/pages/**/*",
"src/components/**/*" "src/components/**/*"
] ],
"oakBuildChecks": {
"context": {
"checkAsyncContext": true,
"targetModules": ["context/BackendRuntimeContext"]
}
}
}`; }`;
} }
function tsConfigPathsJsonContent(deps) { function tsConfigPathsJsonContent(deps) {

View File

@ -1,6 +1,6 @@
{ {
"name": "@xuchangzju/oak-cli", "name": "@xuchangzju/oak-cli",
"version": "4.0.30", "version": "4.0.33",
"description": "client for oak framework", "description": "client for oak framework",
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {
@ -112,9 +112,9 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.5.3", "mini-css-extract-plugin": "^2.5.3",
"node-watch": "^0.7.4", "node-watch": "^0.7.4",
"oak-backend-base": "^4.1.25", "oak-backend-base": "file:../oak-backend-base",
"oak-domain": "^5.1.31", "oak-domain": "file:../oak-domain",
"oak-frontend-base": "^5.3.43", "oak-frontend-base": "file:../oak-frontend-base",
"parse-asn1": "5.1.6", "parse-asn1": "5.1.6",
"postcss": "^8.4.4", "postcss": "^8.4.4",
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",

View File

@ -9,7 +9,7 @@ import KoaBody from 'koa-body';
import { AppLoader, getClusterInfo, ClusterAppLoader } from 'oak-backend-base'; import { AppLoader, getClusterInfo, ClusterAppLoader } from 'oak-backend-base';
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext'; 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 { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { AsyncRowStore, AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { AsyncRowStore, AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
@ -25,7 +25,6 @@ import mount from 'koa-mount';
import chalk from 'chalk'; import chalk from 'chalk';
import { checkNodeVersion, randomString } from '../utils'; import { checkNodeVersion, randomString } from '../utils';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { LogFormatter, polyfillConsole, removePolyfill } from './polyfill';
checkNodeVersion() checkNodeVersion()
@ -216,7 +215,7 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
if (routine) { if (routine) {
// 如果传入了routine执行完成后就结束 // 如果传入了routine执行完成后就结束
const result = await appLoader.execRoutine(routine); const result = await appLoader.execRoutine(routine);
await appLoader.unmount(); // await appLoader.unmount(); // 不卸载,在进程退出时会自动卸载
return result; return result;
} }
@ -245,26 +244,32 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
console.error(err); console.error(err);
const { request } = ctx; const { request } = ctx;
const exception = const exception =
err instanceof OakException isOakException(err)
? err ? err
: new OakException<ED>( : new OakException<ED>(
serverConfiguration?.internalExceptionMask || serverConfiguration?.internalExceptionMask ||
ExceptionMask ExceptionMask
); );
const { body } = connector.serializeException( const { body, headers } = connector.serializeException(
exception, exception,
request.headers, request.headers,
request.body request.body
); );
ctx.response.body = body; ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key])
})
return; return;
} }
}); });
koa.use( koa.use(
KoaBody(Object.assign({ KoaBody(Object.assign({
multipart: true, multipart: true,
}, serverConfiguration.koaBody)) }, serverConfiguration.koaBody))
); );
// 注册自定义中间件 // 注册自定义中间件
if (serverConfiguration.middleware) { if (serverConfiguration.middleware) {
if (Array.isArray(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-Origin', '*');
ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader())); ctx.set('Access-Control-Allow-Headers', corsHeaders.concat(connector.getCorsHeader()));
ctx.set('Access-Control-Allow-Methods', corsMethods); 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') { if (ctx.method == 'OPTIONS') {
ctx.body = 200; ctx.body = 200;
} else { } 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) => { router.post(connector.getRouter(), async (ctx) => {
const { request } = ctx; const { request } = ctx;
const { contextString, aspectName, data } = connector.parseRequest( const { contextString, aspectName, data } = await connector.parseRequest(
request.headers, request.headers,
request.body, request.body,
request.files request.files
); );
const { result, opRecords, message } = await appLoader.execAspect( let result: any;
aspectName, let opRecords: OpRecord<ED>[] = [];
request.headers, let message: string | undefined = undefined;
contextString,
data 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( const { body, headers } = await connector.serializeResult(
result, result,
opRecords, opRecords,
@ -350,6 +387,12 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
message message
); );
ctx.response.body = body; ctx.response.body = body;
// headers 要拼上
Object.keys(headers || {}).forEach(key => {
ctx.set(key, headers?.[key])
})
return; return;
}); });
@ -504,7 +547,6 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
await koa.removeAllListeners(); await koa.removeAllListeners();
await appLoader.execStopRoutines(); await appLoader.execStopRoutines();
await appLoader.unmount(); await appLoader.unmount();
removePolyfill("startup");
} catch (err) { } catch (err) {
console.error('关闭服务器时出错:', err); console.error('关闭服务器时出错:', err);
} }
@ -519,15 +561,15 @@ export async function startup<ED extends EntityDict & BaseEntityDict, FrontCxt e
return; return;
} }
shutdownStarted = true; shutdownStarted = true;
console.log(`\n收到 ${signal} 信号,准备关闭服务器...`); console.log(`\n收到 ${signal} 信号,准备关闭服务器...`);
// 移除所有信号处理器,防止重复触发 // 移除所有信号处理器,防止重复触发
process.removeAllListeners('SIGINT'); process.removeAllListeners('SIGINT');
process.removeAllListeners('SIGTERM'); process.removeAllListeners('SIGTERM');
process.removeAllListeners('SIGQUIT'); process.removeAllListeners('SIGQUIT');
process.removeAllListeners('SIGHUP'); process.removeAllListeners('SIGHUP');
try { try {
await shutdown(); await shutdown();
process.exit(0); process.exit(0);

211
src/server/timer-manager.ts Normal file
View File

@ -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();

View File

@ -7,6 +7,7 @@ import fs from "fs";
import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore"; import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore";
import { LogFormatter, LogFormatterProp, polyfillConsole } from "./polyfill"; import { LogFormatter, LogFormatterProp, polyfillConsole } from "./polyfill";
import { prepareSingleFileReplaceTscAliasPaths, SingleFileReplacer } from 'tsc-alias'; import { prepareSingleFileReplaceTscAliasPaths, SingleFileReplacer } from 'tsc-alias';
import { clearAllTimers, hookTimers } from "./timer-manager";
/* /*
* *
@ -399,6 +400,7 @@ const createServerManager = (
console.warn("----> Starting service......"); console.warn("----> Starting service......");
try { try {
clearAllTimers();
// 这里注意要在require之前因为require会触发编译 // 这里注意要在require之前因为require会触发编译
const { startup } = require('./start') as { const { startup } = require('./start') as {
startup: (pwd: string, connector: any) => Promise<() => Promise<void>>; startup: (pwd: string, connector: any) => Promise<() => Promise<void>>;
@ -477,11 +479,12 @@ const createCompiler = async (
const compileTask = async (task: CompileTask): Promise<CompileResult> => { const compileTask = async (task: CompileTask): Promise<CompileResult> => {
const { filePath, changeType } = task; const { filePath, changeType } = task;
const modulePath = pathLib.resolve(filePath);
// 判断文件类型 // 判断文件类型
if (!filePath.endsWith(".ts")) { if (!filePath.endsWith(".ts")) {
// 处理非TypeScript文件 (如JSON文件) // 处理非TypeScript文件 (如JSON文件)
if (filePath.endsWith(".json")) { if (filePath.endsWith(".json")) {
const targetPath = filePath.replace( const targetPath = modulePath.replace(
pathLib.join(projectPath, "src"), pathLib.join(projectPath, "src"),
pathLib.join(projectPath, "lib") pathLib.join(projectPath, "lib")
); );
@ -533,7 +536,6 @@ const createCompiler = async (
console.clear(); console.clear();
console.warn(`File ${filePath} has been ${changeType}d`); console.warn(`File ${filePath} has been ${changeType}d`);
const modulePath = pathLib.resolve(filePath);
const libPath = modulePath const libPath = modulePath
.replace(pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib")) .replace(pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib"))
.replace(/\.ts$/, ".js"); .replace(/\.ts$/, ".js");
@ -879,6 +881,9 @@ export const watch = async (
polyfillLoader(); polyfillLoader();
realConfig.polyfill.console.enable && polyfillConsole("watch", enableTrace, realConfig.polyfill?.console?.formatter); realConfig.polyfill.console.enable && polyfillConsole("watch", enableTrace, realConfig.polyfill?.console?.formatter);
// 监听定时器相关的API,确保定时器在重启后不会重复触发
hookTimers()
// 初始编译检查 // 初始编译检查
const initialCompile = () => { const initialCompile = () => {
const serverConfigFile = pathLib.join(projectPath, "lib/configuration/server.js"); const serverConfigFile = pathLib.join(projectPath, "lib/configuration/server.js");

View File

@ -92,7 +92,7 @@ export function packageJsonContent({
"build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze", "build-analyze:mp:staging": "${cliBinName} build --target mp --mode staging --analyze",
"build:mp": "${cliBinName} build --target mp --mode production", "build:mp": "${cliBinName} build --target mp --mode production",
"build-analyze:mp": "${cliBinName} build --target mp --mode production --analyze", "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": "${cliBinName} start --target web --mode development --devMode frontend",
"start:web:server": "${cliBinName} start --target web --mode development", "start:web:server": "${cliBinName} start --target web --mode development",
"start:native": "${cliBinName} start --target rn --mode development --devMode frontend", "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-sourcemap:web": "${cliBinName} build --target web --mode production --sourcemap",
"build-analyze:web": "${cliBinName} build --target web --mode production --analyze", "build-analyze:web": "${cliBinName} build --target web --mode production --analyze",
"build-sourcemap-analyze:web": "${cliBinName} build --target web --mode production --sourcemap --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", "prebuild": "npm run make:locale",
"run:ios": "oak-cli run -p ios", "run:ios": "oak-cli run -p ios",
"run:android": "oak-cli run -p android", "run:android": "oak-cli run -p android",
@ -406,7 +406,13 @@ export function tsConfigBuildJsonContent() {
"test", "test",
"src/pages/**/*", "src/pages/**/*",
"src/components/**/*" "src/components/**/*"
] ],
"oakBuildChecks": {
"context": {
"checkAsyncContext": true,
"targetModules": ["context/BackendRuntimeContext"]
}
}
}`; }`;
} }

View File

@ -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);