dynapm 1.0.11 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +8 -3
- package/README_zh.md +8 -3
- package/dist/src/config/types.d.ts +52 -1
- package/dist/src/core/admin-api.d.ts +72 -0
- package/dist/src/core/gateway.d.ts +43 -6
- package/dist/src/index.js +968 -157
- package/dist/src/utils/format.d.ts +6 -0
- package/package.json +4 -4
package/dist/src/index.js
CHANGED
|
@@ -2542,20 +2542,26 @@ var __webpack_modules__ = {
|
|
|
2542
2542
|
net: function(module) {
|
|
2543
2543
|
module.exports = require("net");
|
|
2544
2544
|
},
|
|
2545
|
+
"node:net": function(module) {
|
|
2546
|
+
module.exports = require("node:net");
|
|
2547
|
+
},
|
|
2548
|
+
"node:url": function(module) {
|
|
2549
|
+
module.exports = require("node:url");
|
|
2550
|
+
},
|
|
2545
2551
|
stream: function(module) {
|
|
2546
2552
|
module.exports = require("stream");
|
|
2547
2553
|
},
|
|
2548
2554
|
tls: function(module) {
|
|
2549
2555
|
module.exports = require("tls");
|
|
2550
2556
|
},
|
|
2557
|
+
"uWebSockets.js": function(module) {
|
|
2558
|
+
module.exports = require("uWebSockets.js");
|
|
2559
|
+
},
|
|
2551
2560
|
url: function(module) {
|
|
2552
2561
|
module.exports = require("url");
|
|
2553
2562
|
},
|
|
2554
2563
|
zlib: function(module) {
|
|
2555
2564
|
module.exports = require("zlib");
|
|
2556
|
-
},
|
|
2557
|
-
"uWebSockets.js": function(module) {
|
|
2558
|
-
module.exports = import("uWebSockets.js");
|
|
2559
2565
|
}
|
|
2560
2566
|
};
|
|
2561
2567
|
var __webpack_module_cache__ = {};
|
|
@@ -2581,6 +2587,41 @@ function __webpack_require__(moduleId) {
|
|
|
2581
2587
|
return getter;
|
|
2582
2588
|
};
|
|
2583
2589
|
})();
|
|
2590
|
+
(()=>{
|
|
2591
|
+
var getProto = Object.getPrototypeOf ? function(obj) {
|
|
2592
|
+
return Object.getPrototypeOf(obj);
|
|
2593
|
+
} : function(obj) {
|
|
2594
|
+
return obj.__proto__;
|
|
2595
|
+
};
|
|
2596
|
+
var leafPrototypes;
|
|
2597
|
+
__webpack_require__.t = function(value, mode) {
|
|
2598
|
+
if (1 & mode) value = this(value);
|
|
2599
|
+
if (8 & mode) return value;
|
|
2600
|
+
if ('object' == typeof value && value) {
|
|
2601
|
+
if (4 & mode && value.__esModule) return value;
|
|
2602
|
+
if (16 & mode && 'function' == typeof value.then) return value;
|
|
2603
|
+
}
|
|
2604
|
+
var ns = Object.create(null);
|
|
2605
|
+
__webpack_require__.r(ns);
|
|
2606
|
+
var def = {};
|
|
2607
|
+
leafPrototypes = leafPrototypes || [
|
|
2608
|
+
null,
|
|
2609
|
+
getProto({}),
|
|
2610
|
+
getProto([]),
|
|
2611
|
+
getProto(getProto)
|
|
2612
|
+
];
|
|
2613
|
+
for(var current = 2 & mode && value; 'object' == typeof current && !~leafPrototypes.indexOf(current); current = getProto(current))Object.getOwnPropertyNames(current).forEach(function(key) {
|
|
2614
|
+
def[key] = function() {
|
|
2615
|
+
return value[key];
|
|
2616
|
+
};
|
|
2617
|
+
});
|
|
2618
|
+
def['default'] = function() {
|
|
2619
|
+
return value;
|
|
2620
|
+
};
|
|
2621
|
+
__webpack_require__.d(ns, def);
|
|
2622
|
+
return ns;
|
|
2623
|
+
};
|
|
2624
|
+
})();
|
|
2584
2625
|
(()=>{
|
|
2585
2626
|
__webpack_require__.d = function(exports1, definition) {
|
|
2586
2627
|
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
@@ -2594,6 +2635,16 @@ function __webpack_require__(moduleId) {
|
|
|
2594
2635
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
2595
2636
|
};
|
|
2596
2637
|
})();
|
|
2638
|
+
(()=>{
|
|
2639
|
+
__webpack_require__.r = function(exports1) {
|
|
2640
|
+
if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
2641
|
+
value: 'Module'
|
|
2642
|
+
});
|
|
2643
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
2644
|
+
value: true
|
|
2645
|
+
});
|
|
2646
|
+
};
|
|
2647
|
+
})();
|
|
2597
2648
|
var __webpack_exports__ = {};
|
|
2598
2649
|
(()=>{
|
|
2599
2650
|
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
@@ -2653,6 +2704,11 @@ var __webpack_exports__ = {};
|
|
|
2653
2704
|
}
|
|
2654
2705
|
lock = (async ()=>{
|
|
2655
2706
|
try {
|
|
2707
|
+
const alreadyRunning = await this.isRunning(service);
|
|
2708
|
+
if (alreadyRunning) {
|
|
2709
|
+
console.log(`[${service.name}] 服务已在运行,跳过启动`);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2656
2712
|
console.log(`[${service.name}] 正在启动...`);
|
|
2657
2713
|
const result = await this.executor.execute(service.commands.start, {
|
|
2658
2714
|
cwd: service.commands.cwd,
|
|
@@ -2683,30 +2739,309 @@ var __webpack_exports__ = {};
|
|
|
2683
2739
|
service._state.status = 'offline';
|
|
2684
2740
|
}
|
|
2685
2741
|
}
|
|
2686
|
-
|
|
2687
|
-
var
|
|
2688
|
-
|
|
2689
|
-
var
|
|
2690
|
-
|
|
2691
|
-
var external_node_https_default = /*#__PURE__*/ __webpack_require__.n(external_node_https_namespaceObject);
|
|
2692
|
-
const external_node_url_namespaceObject = require("node:url");
|
|
2742
|
+
var external_uWebSockets_js_ = __webpack_require__("uWebSockets.js");
|
|
2743
|
+
var external_uWebSockets_js_default = /*#__PURE__*/ __webpack_require__.n(external_uWebSockets_js_);
|
|
2744
|
+
var external_node_net_ = __webpack_require__("node:net");
|
|
2745
|
+
var external_node_net_default = /*#__PURE__*/ __webpack_require__.n(external_node_net_);
|
|
2746
|
+
var external_node_url_ = __webpack_require__("node:url");
|
|
2693
2747
|
__webpack_require__("./node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/stream.js");
|
|
2694
2748
|
__webpack_require__("./node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/receiver.js");
|
|
2695
2749
|
__webpack_require__("./node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/sender.js");
|
|
2696
2750
|
var websocket = __webpack_require__("./node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/websocket.js");
|
|
2697
2751
|
__webpack_require__("./node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/websocket-server.js");
|
|
2698
2752
|
const wrapper = websocket;
|
|
2753
|
+
class AdminApiHandler {
|
|
2754
|
+
config;
|
|
2755
|
+
logger;
|
|
2756
|
+
hostnameRoutes;
|
|
2757
|
+
portRoutes;
|
|
2758
|
+
serviceManager;
|
|
2759
|
+
constructor(config, logger, hostnameRoutes, portRoutes, serviceManager){
|
|
2760
|
+
this.config = config;
|
|
2761
|
+
this.logger = logger;
|
|
2762
|
+
this.hostnameRoutes = hostnameRoutes;
|
|
2763
|
+
this.portRoutes = portRoutes;
|
|
2764
|
+
this.serviceManager = serviceManager;
|
|
2765
|
+
}
|
|
2766
|
+
isIpAllowed(ip) {
|
|
2767
|
+
if (!this.config.adminApi?.allowedIps || 0 === this.config.adminApi.allowedIps.length) return true;
|
|
2768
|
+
return this.config.adminApi.allowedIps.includes(ip);
|
|
2769
|
+
}
|
|
2770
|
+
isAuthenticated(authHeader) {
|
|
2771
|
+
if (!this.config.adminApi?.authToken) return true;
|
|
2772
|
+
if (!authHeader) return false;
|
|
2773
|
+
const token = authHeader.replace('Bearer ', '');
|
|
2774
|
+
return token === this.config.adminApi.authToken;
|
|
2775
|
+
}
|
|
2776
|
+
getServiceUptime(service) {
|
|
2777
|
+
if ('online' === service._state.status && service._state.startTime) return service._state.totalUptime + (Date.now() - service._state.startTime);
|
|
2778
|
+
return service._state.totalUptime;
|
|
2779
|
+
}
|
|
2780
|
+
handleAdminApi(res, req) {
|
|
2781
|
+
const ip = req.getHeader('x-forwarded-for')?.split(',')[0]?.trim() || req.getHeader('cf-connecting-ip') || '127.0.0.1';
|
|
2782
|
+
if (!this.isIpAllowed(ip)) {
|
|
2783
|
+
res.cork(()=>{
|
|
2784
|
+
res.writeStatus('403 Forbidden');
|
|
2785
|
+
res.end('Forbidden');
|
|
2786
|
+
});
|
|
2787
|
+
this.logger.warn({
|
|
2788
|
+
msg: `🚫 [Admin API] 拒绝访问: ${ip}`
|
|
2789
|
+
});
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
const authHeader = req.getHeader('authorization');
|
|
2793
|
+
if (!this.isAuthenticated(authHeader)) {
|
|
2794
|
+
res.cork(()=>{
|
|
2795
|
+
res.writeStatus('401 Unauthorized');
|
|
2796
|
+
res.writeHeader('WWW-Authenticate', 'Bearer');
|
|
2797
|
+
res.end('Unauthorized');
|
|
2798
|
+
});
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
const url = req.getUrl();
|
|
2802
|
+
const method = req.getMethod();
|
|
2803
|
+
if ('/_dynapm/api/services' === url && 'get' === method.toLowerCase()) this.getServicesList(res);
|
|
2804
|
+
else if (url.startsWith('/_dynapm/api/services/') && 'get' === method.toLowerCase()) {
|
|
2805
|
+
const serviceName = url.split('/')[4];
|
|
2806
|
+
this.getServiceDetail(res, serviceName);
|
|
2807
|
+
} else if (url.endsWith('/stop') && 'post' === method.toLowerCase()) {
|
|
2808
|
+
const parts = url.split('/');
|
|
2809
|
+
const serviceName = parts[4];
|
|
2810
|
+
this.stopService(res, serviceName);
|
|
2811
|
+
} else if (url.endsWith('/start') && 'post' === method.toLowerCase()) {
|
|
2812
|
+
const parts = url.split('/');
|
|
2813
|
+
const serviceName = parts[4];
|
|
2814
|
+
this.startService(res, serviceName);
|
|
2815
|
+
} else if ('/_dynapm/api/events' === url && 'get' === method.toLowerCase()) this.handleEventStream(res);
|
|
2816
|
+
else res.cork(()=>{
|
|
2817
|
+
res.writeStatus('404 Not Found');
|
|
2818
|
+
res.end('Not Found');
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
getServicesList(res) {
|
|
2822
|
+
const serviceMap = new Map();
|
|
2823
|
+
for (const mapping of this.hostnameRoutes.values())serviceMap.set(mapping.service.name, mapping.service);
|
|
2824
|
+
for (const mapping of this.portRoutes.values())serviceMap.set(mapping.service.name, mapping.service);
|
|
2825
|
+
const services = Array.from(serviceMap.values()).map((service)=>({
|
|
2826
|
+
name: service.name,
|
|
2827
|
+
base: service.base,
|
|
2828
|
+
status: service._state.status,
|
|
2829
|
+
uptime: this.getServiceUptime(service),
|
|
2830
|
+
lastAccessTime: service._state.lastAccessTime,
|
|
2831
|
+
activeConnections: service._state.activeConnections,
|
|
2832
|
+
idleTimeout: service.idleTimeout,
|
|
2833
|
+
proxyOnly: service.proxyOnly || false,
|
|
2834
|
+
pid: service._state.pid
|
|
2835
|
+
}));
|
|
2836
|
+
res.cork(()=>{
|
|
2837
|
+
res.writeHeader('Content-Type', 'application/json');
|
|
2838
|
+
res.end(JSON.stringify({
|
|
2839
|
+
services
|
|
2840
|
+
}));
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
getServiceDetail(res, serviceName) {
|
|
2844
|
+
const mapping = Array.from(this.hostnameRoutes.values()).find((m)=>m.service.name === serviceName);
|
|
2845
|
+
this.logger.info({
|
|
2846
|
+
msg: `🔍 [Admin API] 查找服务: ${serviceName}, 找到: ${mapping?.service.name || 'null'}`
|
|
2847
|
+
});
|
|
2848
|
+
if (!mapping) {
|
|
2849
|
+
res.cork(()=>{
|
|
2850
|
+
res.writeStatus('404 Not Found');
|
|
2851
|
+
res.end(JSON.stringify({
|
|
2852
|
+
error: 'Service not found'
|
|
2853
|
+
}));
|
|
2854
|
+
});
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
const service = mapping.service;
|
|
2858
|
+
const detail = {
|
|
2859
|
+
name: service.name,
|
|
2860
|
+
base: service.base,
|
|
2861
|
+
status: service._state.status,
|
|
2862
|
+
uptime: this.getServiceUptime(service),
|
|
2863
|
+
lastAccessTime: service._state.lastAccessTime,
|
|
2864
|
+
activeConnections: service._state.activeConnections,
|
|
2865
|
+
idleTimeout: service.idleTimeout,
|
|
2866
|
+
startTimeout: service.startTimeout,
|
|
2867
|
+
proxyOnly: service.proxyOnly || false,
|
|
2868
|
+
pid: service._state.pid,
|
|
2869
|
+
healthCheck: service.healthCheck || {
|
|
2870
|
+
type: 'tcp'
|
|
2871
|
+
},
|
|
2872
|
+
startCount: service._state.startCount,
|
|
2873
|
+
totalUptime: service._state.totalUptime
|
|
2874
|
+
};
|
|
2875
|
+
res.cork(()=>{
|
|
2876
|
+
res.writeHeader('Content-Type', 'application/json');
|
|
2877
|
+
res.end(JSON.stringify(detail));
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
async stopService(res, serviceName) {
|
|
2881
|
+
const mapping = Array.from(this.hostnameRoutes.values()).find((m)=>m.service.name === serviceName);
|
|
2882
|
+
if (!mapping) {
|
|
2883
|
+
res.cork(()=>{
|
|
2884
|
+
res.writeStatus('404 Not Found');
|
|
2885
|
+
res.end(JSON.stringify({
|
|
2886
|
+
error: 'Service not found'
|
|
2887
|
+
}));
|
|
2888
|
+
});
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
const service = mapping.service;
|
|
2892
|
+
if ('online' !== service._state.status) {
|
|
2893
|
+
res.cork(()=>{
|
|
2894
|
+
res.writeStatus('400 Bad Request');
|
|
2895
|
+
res.end(JSON.stringify({
|
|
2896
|
+
error: 'Service is not online'
|
|
2897
|
+
}));
|
|
2898
|
+
});
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
try {
|
|
2902
|
+
service._state.status = 'stopping';
|
|
2903
|
+
if (service._state.startTime) {
|
|
2904
|
+
service._state.totalUptime += Date.now() - service._state.startTime;
|
|
2905
|
+
service._state.startTime = void 0;
|
|
2906
|
+
}
|
|
2907
|
+
await this.serviceManager.stop(service);
|
|
2908
|
+
service._state.status = 'offline';
|
|
2909
|
+
res.cork(()=>{
|
|
2910
|
+
res.writeHeader('Content-Type', 'application/json');
|
|
2911
|
+
res.end(JSON.stringify({
|
|
2912
|
+
success: true,
|
|
2913
|
+
message: `服务 ${service.name} 已停止`
|
|
2914
|
+
}));
|
|
2915
|
+
});
|
|
2916
|
+
} catch (error) {
|
|
2917
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2918
|
+
service._state.status = 'online';
|
|
2919
|
+
res.cork(()=>{
|
|
2920
|
+
res.writeStatus('500 Internal Server Error');
|
|
2921
|
+
res.end(JSON.stringify({
|
|
2922
|
+
error: message
|
|
2923
|
+
}));
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
async startService(res, serviceName) {
|
|
2928
|
+
const mapping = Array.from(this.hostnameRoutes.values()).find((m)=>m.service.name === serviceName);
|
|
2929
|
+
if (!mapping) {
|
|
2930
|
+
res.cork(()=>{
|
|
2931
|
+
res.writeStatus('404 Not Found');
|
|
2932
|
+
res.end(JSON.stringify({
|
|
2933
|
+
error: 'Service not found'
|
|
2934
|
+
}));
|
|
2935
|
+
});
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
const service = mapping.service;
|
|
2939
|
+
if ('online' === service._state.status || 'starting' === service._state.status) {
|
|
2940
|
+
res.cork(()=>{
|
|
2941
|
+
res.writeStatus('400 Bad Request');
|
|
2942
|
+
res.end(JSON.stringify({
|
|
2943
|
+
error: 'Service is already running or starting'
|
|
2944
|
+
}));
|
|
2945
|
+
});
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
try {
|
|
2949
|
+
service._state.status = 'starting';
|
|
2950
|
+
this.serviceManager.start(service).catch((err)=>{
|
|
2951
|
+
this.logger.error({
|
|
2952
|
+
msg: `❌ [${service.name}] 启动失败`,
|
|
2953
|
+
error: err.message
|
|
2954
|
+
});
|
|
2955
|
+
service._state.status = 'offline';
|
|
2956
|
+
});
|
|
2957
|
+
const waitStartTime = Date.now();
|
|
2958
|
+
let isReady = false;
|
|
2959
|
+
while(Date.now() - waitStartTime < service.startTimeout){
|
|
2960
|
+
isReady = await checkTcpPort(service.base);
|
|
2961
|
+
if (isReady) break;
|
|
2962
|
+
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
2963
|
+
}
|
|
2964
|
+
if (!isReady) {
|
|
2965
|
+
service._state.status = 'offline';
|
|
2966
|
+
res.cork(()=>{
|
|
2967
|
+
res.writeStatus('503 Service Unavailable');
|
|
2968
|
+
res.end(JSON.stringify({
|
|
2969
|
+
error: 'Service start timeout'
|
|
2970
|
+
}));
|
|
2971
|
+
});
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
service._state.status = 'online';
|
|
2975
|
+
service._state.startTime = Date.now();
|
|
2976
|
+
service._state.startCount++;
|
|
2977
|
+
res.cork(()=>{
|
|
2978
|
+
res.writeHeader('Content-Type', 'application/json');
|
|
2979
|
+
res.end(JSON.stringify({
|
|
2980
|
+
success: true,
|
|
2981
|
+
message: `服务 ${service.name} 已启动`
|
|
2982
|
+
}));
|
|
2983
|
+
});
|
|
2984
|
+
} catch (error) {
|
|
2985
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2986
|
+
service._state.status = 'offline';
|
|
2987
|
+
res.cork(()=>{
|
|
2988
|
+
res.writeStatus('500 Internal Server Error');
|
|
2989
|
+
res.end(JSON.stringify({
|
|
2990
|
+
error: message
|
|
2991
|
+
}));
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
handleEventStream(res) {
|
|
2996
|
+
res.cork(()=>{
|
|
2997
|
+
res.writeStatus('200 OK');
|
|
2998
|
+
res.writeHeader('Content-Type', 'text/event-stream');
|
|
2999
|
+
res.writeHeader('Cache-Control', 'no-cache');
|
|
3000
|
+
res.writeHeader('Connection', 'keep-alive');
|
|
3001
|
+
res.writeHeader('X-Accel-Buffering', 'no');
|
|
3002
|
+
});
|
|
3003
|
+
res.cork(()=>{
|
|
3004
|
+
res.end(`event: connected\ndata: {"timestamp":${Date.now()}}\n\n`);
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
function checkTcpPort(url) {
|
|
3009
|
+
const { URL } = __webpack_require__("node:url");
|
|
3010
|
+
const net = __webpack_require__("node:net");
|
|
3011
|
+
const parsed = new URL(url);
|
|
3012
|
+
const host = parsed.hostname;
|
|
3013
|
+
const port = parseInt(parsed.port || ('https:' === parsed.protocol ? '443' : '80'));
|
|
3014
|
+
return new Promise((resolve)=>{
|
|
3015
|
+
const socket = net.createConnection({
|
|
3016
|
+
host,
|
|
3017
|
+
port,
|
|
3018
|
+
timeout: 100
|
|
3019
|
+
}, ()=>{
|
|
3020
|
+
socket.destroy();
|
|
3021
|
+
resolve(true);
|
|
3022
|
+
});
|
|
3023
|
+
socket.on('error', ()=>{
|
|
3024
|
+
socket.destroy();
|
|
3025
|
+
resolve(false);
|
|
3026
|
+
});
|
|
3027
|
+
socket.on('timeout', ()=>{
|
|
3028
|
+
socket.destroy();
|
|
3029
|
+
resolve(false);
|
|
3030
|
+
});
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
2699
3033
|
function formatTime(ms) {
|
|
2700
3034
|
if (ms < 1000) return `${ms}ms`;
|
|
2701
3035
|
return `${(ms / 1000).toFixed(2)}s`;
|
|
2702
3036
|
}
|
|
3037
|
+
const external_undici_namespaceObject = require("undici");
|
|
2703
3038
|
const GatewayConstants = {
|
|
2704
3039
|
IDLE_CHECK_INTERVAL: 3000,
|
|
2705
3040
|
TCP_CHECK_TIMEOUT: 100,
|
|
2706
3041
|
BACKEND_READY_CHECK_DELAY: 50
|
|
2707
3042
|
};
|
|
2708
|
-
function
|
|
2709
|
-
const parsed = new
|
|
3043
|
+
function gateway_checkTcpPort(url) {
|
|
3044
|
+
const parsed = new external_node_url_.URL(url);
|
|
2710
3045
|
const host = parsed.hostname;
|
|
2711
3046
|
const port = parseInt(parsed.port || ('https:' === parsed.protocol ? '443' : '80'));
|
|
2712
3047
|
return new Promise((resolve)=>{
|
|
@@ -2731,46 +3066,112 @@ var __webpack_exports__ = {};
|
|
|
2731
3066
|
class Gateway {
|
|
2732
3067
|
config;
|
|
2733
3068
|
serviceManager;
|
|
2734
|
-
|
|
3069
|
+
hostnameRoutes;
|
|
3070
|
+
portRoutes;
|
|
2735
3071
|
logger;
|
|
3072
|
+
logging;
|
|
3073
|
+
adminApi;
|
|
2736
3074
|
constructor(config, logger){
|
|
2737
3075
|
this.config = config;
|
|
2738
3076
|
this.serviceManager = new ServiceManager();
|
|
2739
|
-
this.
|
|
3077
|
+
this.hostnameRoutes = new Map();
|
|
3078
|
+
this.portRoutes = new Map();
|
|
2740
3079
|
this.logger = logger;
|
|
3080
|
+
this.logging = {
|
|
3081
|
+
enableRequestLog: config.logging?.enableRequestLog ?? false,
|
|
3082
|
+
enableWebSocketLog: config.logging?.enableWebSocketLog ?? false,
|
|
3083
|
+
enablePerformanceLog: config.logging?.enablePerformanceLog ?? false
|
|
3084
|
+
};
|
|
3085
|
+
this.adminApi = new AdminApiHandler(config, logger, this.hostnameRoutes, this.portRoutes, this.serviceManager);
|
|
2741
3086
|
this.initServices();
|
|
2742
3087
|
this.initIdleChecker();
|
|
2743
3088
|
}
|
|
2744
3089
|
initServices() {
|
|
2745
|
-
|
|
3090
|
+
if (!this.config.services) return;
|
|
3091
|
+
console.log('[DynaPM] 初始化服务...');
|
|
3092
|
+
console.log('[DynaPM] 服务数量:', Object.keys(this.config.services).length);
|
|
3093
|
+
for (const service of Object.values(this.config.services)){
|
|
2746
3094
|
service._state = {
|
|
2747
|
-
status: 'offline',
|
|
3095
|
+
status: service.proxyOnly ? 'online' : 'offline',
|
|
2748
3096
|
lastAccessTime: Date.now(),
|
|
2749
|
-
activeConnections: 0
|
|
3097
|
+
activeConnections: 0,
|
|
3098
|
+
startCount: 0,
|
|
3099
|
+
totalUptime: 0
|
|
2750
3100
|
};
|
|
2751
|
-
|
|
3101
|
+
const routes = service.routes || [];
|
|
3102
|
+
if (0 === routes.length) {
|
|
3103
|
+
console.warn(`[DynaPM] ⚠️ [${service.name}] 没有配置路由`);
|
|
3104
|
+
continue;
|
|
3105
|
+
}
|
|
3106
|
+
console.log(`[DynaPM] ✅ [${service.name}] 配置了 ${routes.length} 个路由:`);
|
|
3107
|
+
for (const route of routes){
|
|
3108
|
+
const targetUrl = new external_node_url_.URL(route.target);
|
|
3109
|
+
const undiciClient = new external_undici_namespaceObject.Client(route.target, {
|
|
3110
|
+
keepAliveTimeout: 60000,
|
|
3111
|
+
pipelining: 1
|
|
3112
|
+
});
|
|
3113
|
+
const mapping = {
|
|
3114
|
+
service,
|
|
3115
|
+
target: route.target,
|
|
3116
|
+
targetUrl,
|
|
3117
|
+
undiciClient
|
|
3118
|
+
};
|
|
3119
|
+
if ('host' === route.type) {
|
|
3120
|
+
const hostname = route.value;
|
|
3121
|
+
this.hostnameRoutes.set(hostname, mapping);
|
|
3122
|
+
console.log(`[DynaPM] └─ hostname: ${hostname} -> ${route.target}`);
|
|
3123
|
+
} else if ('port' === route.type) {
|
|
3124
|
+
const port = route.value;
|
|
3125
|
+
this.portRoutes.set(port, mapping);
|
|
3126
|
+
console.log(`[DynaPM] └─ port: ${port} -> ${route.target}`);
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
2752
3129
|
}
|
|
3130
|
+
const hostnameCount = this.hostnameRoutes.size;
|
|
3131
|
+
const portCount = this.portRoutes.size;
|
|
3132
|
+
console.log(`[DynaPM] 📊 共配置 ${hostnameCount} 个 hostname 映射, ${portCount} 个端口绑定`);
|
|
3133
|
+
this.logger.info({
|
|
3134
|
+
msg: `📊 共配置 ${hostnameCount} 个 hostname 映射, ${portCount} 个端口绑定`
|
|
3135
|
+
});
|
|
2753
3136
|
}
|
|
2754
3137
|
initIdleChecker() {
|
|
2755
3138
|
setInterval(()=>{
|
|
2756
3139
|
const now = Date.now();
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
});
|
|
2766
|
-
});
|
|
2767
|
-
service._state.status = 'offline';
|
|
3140
|
+
const checkedServices = new Set();
|
|
3141
|
+
for (const mapping of this.hostnameRoutes.values())if (!checkedServices.has(mapping.service)) {
|
|
3142
|
+
checkedServices.add(mapping.service);
|
|
3143
|
+
this.checkIdleService(mapping.service, now);
|
|
3144
|
+
}
|
|
3145
|
+
for (const mapping of this.portRoutes.values())if (!checkedServices.has(mapping.service)) {
|
|
3146
|
+
checkedServices.add(mapping.service);
|
|
3147
|
+
this.checkIdleService(mapping.service, now);
|
|
2768
3148
|
}
|
|
2769
3149
|
}, GatewayConstants.IDLE_CHECK_INTERVAL);
|
|
2770
3150
|
}
|
|
2771
|
-
|
|
3151
|
+
checkIdleService(service, now) {
|
|
3152
|
+
if (service.proxyOnly) return;
|
|
3153
|
+
if ('online' === service._state.status && 0 === service._state.activeConnections && now - service._state.lastAccessTime > service.idleTimeout) {
|
|
3154
|
+
this.logger.info({
|
|
3155
|
+
msg: `🛌 [${service.name}] 闲置超时,正在停止...`
|
|
3156
|
+
});
|
|
3157
|
+
service._state.status = 'stopping';
|
|
3158
|
+
if (service._state.startTime) {
|
|
3159
|
+
service._state.totalUptime += now - service._state.startTime;
|
|
3160
|
+
service._state.startTime = void 0;
|
|
3161
|
+
}
|
|
3162
|
+
this.serviceManager.stop(service).catch((err)=>{
|
|
3163
|
+
this.logger.error({
|
|
3164
|
+
msg: `❌ [${service.name}] 停止失败`,
|
|
3165
|
+
error: err.message
|
|
3166
|
+
});
|
|
3167
|
+
}).finally(()=>{
|
|
3168
|
+
service._state.status = 'offline';
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
handlePortBindingRequest(res, req, mapping) {
|
|
3173
|
+
const service = mapping.service;
|
|
2772
3174
|
const startTime = Date.now();
|
|
2773
|
-
const hostname = req.getHeader('host')?.split(':')[0] || '';
|
|
2774
3175
|
const method = req.getMethod();
|
|
2775
3176
|
const url = req.getUrl();
|
|
2776
3177
|
const queryString = req.getQuery();
|
|
@@ -2780,8 +3181,33 @@ var __webpack_exports__ = {};
|
|
|
2780
3181
|
const safeValue = value.replace(/[\r\n]/g, '');
|
|
2781
3182
|
headers[key] = safeValue;
|
|
2782
3183
|
});
|
|
2783
|
-
|
|
2784
|
-
|
|
3184
|
+
service._state.lastAccessTime = Date.now();
|
|
3185
|
+
const needsStart = 'offline' === service._state.status;
|
|
3186
|
+
if (needsStart) this.handleServiceStart(res, mapping, fullUrl, startTime, method, headers);
|
|
3187
|
+
else this.handleDirectProxy(res, mapping, fullUrl, startTime, method, headers);
|
|
3188
|
+
}
|
|
3189
|
+
handleRequest(res, req) {
|
|
3190
|
+
const startTime = Date.now();
|
|
3191
|
+
const hostHeader = req.getHeader('host');
|
|
3192
|
+
let hostname = '';
|
|
3193
|
+
if (hostHeader) {
|
|
3194
|
+
const colonIndex = hostHeader.indexOf(':');
|
|
3195
|
+
hostname = -1 !== colonIndex ? hostHeader.substring(0, colonIndex) : hostHeader;
|
|
3196
|
+
}
|
|
3197
|
+
const method = req.getMethod();
|
|
3198
|
+
const url = req.getUrl();
|
|
3199
|
+
const queryString = req.getQuery();
|
|
3200
|
+
const fullUrl = queryString ? url + '?' + queryString : url;
|
|
3201
|
+
const headers = {};
|
|
3202
|
+
req.forEach((key, value)=>{
|
|
3203
|
+
let safeValue = value;
|
|
3204
|
+
const crIndex = value.indexOf('\r');
|
|
3205
|
+
const lfIndex = value.indexOf('\n');
|
|
3206
|
+
if (-1 !== crIndex || -1 !== lfIndex) safeValue = value.replace(/[\r\n]/g, '');
|
|
3207
|
+
headers[key] = safeValue;
|
|
3208
|
+
});
|
|
3209
|
+
const mapping = this.hostnameRoutes.get(hostname);
|
|
3210
|
+
if (!mapping) {
|
|
2785
3211
|
this.logger.info({
|
|
2786
3212
|
msg: `❌ [${hostname}] ${method} ${fullUrl} - 404`
|
|
2787
3213
|
});
|
|
@@ -2791,17 +3217,70 @@ var __webpack_exports__ = {};
|
|
|
2791
3217
|
});
|
|
2792
3218
|
return;
|
|
2793
3219
|
}
|
|
3220
|
+
const service = mapping.service;
|
|
2794
3221
|
service._state.lastAccessTime = Date.now();
|
|
2795
|
-
const
|
|
2796
|
-
|
|
2797
|
-
|
|
3222
|
+
const status = service._state.status;
|
|
3223
|
+
const needsStart = 'offline' === status || 'stopping' === status;
|
|
3224
|
+
if (needsStart) {
|
|
3225
|
+
if ('stopping' === status) this.handleServiceWithWait(res, mapping, fullUrl, startTime, method, headers);
|
|
3226
|
+
else this.handleServiceStart(res, mapping, fullUrl, startTime, method, headers);
|
|
3227
|
+
} else this.handleDirectProxy(res, mapping, fullUrl, startTime, method, headers);
|
|
2798
3228
|
}
|
|
2799
|
-
|
|
2800
|
-
const
|
|
3229
|
+
async startServiceAndProxy(res, mapping, fullUrl, startTime, method, headers, body) {
|
|
3230
|
+
const service = mapping.service;
|
|
3231
|
+
const target = mapping.target;
|
|
2801
3232
|
this.logger.info({
|
|
2802
3233
|
msg: `🚀 [${service.name}] ${method} ${fullUrl} - 启动服务...`
|
|
2803
3234
|
});
|
|
2804
3235
|
service._state.status = 'starting';
|
|
3236
|
+
try {
|
|
3237
|
+
await this.serviceManager.start(service);
|
|
3238
|
+
const waitStartTime = Date.now();
|
|
3239
|
+
let isReady = false;
|
|
3240
|
+
while(Date.now() - waitStartTime < service.startTimeout){
|
|
3241
|
+
isReady = await gateway_checkTcpPort(target);
|
|
3242
|
+
if (isReady) {
|
|
3243
|
+
const waitDuration = Date.now() - waitStartTime;
|
|
3244
|
+
this.logger.info({
|
|
3245
|
+
msg: `✅ [${service.name}] 服务就绪 (等待${formatTime(waitDuration)})`
|
|
3246
|
+
});
|
|
3247
|
+
break;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
if (!isReady) {
|
|
3251
|
+
service._state.status = 'offline';
|
|
3252
|
+
throw new Error(`服务启动超时: 端口 ${target} 不可用`);
|
|
3253
|
+
}
|
|
3254
|
+
service._state.status = 'online';
|
|
3255
|
+
service._state.startTime = Date.now();
|
|
3256
|
+
service._state.startCount++;
|
|
3257
|
+
await this.forwardProxyRequest(res, mapping, fullUrl, startTime, method, headers, body);
|
|
3258
|
+
} catch (error) {
|
|
3259
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3260
|
+
if ('Client aborted' === message) return;
|
|
3261
|
+
this.logger.error({
|
|
3262
|
+
msg: `❌ [${service.name}] 启动失败`,
|
|
3263
|
+
error: message
|
|
3264
|
+
});
|
|
3265
|
+
try {
|
|
3266
|
+
res.cork(()=>{
|
|
3267
|
+
res.writeStatus('503 Service Unavailable');
|
|
3268
|
+
res.end('Service Unavailable');
|
|
3269
|
+
});
|
|
3270
|
+
} catch (sendErr) {
|
|
3271
|
+
const sendErrMsg = sendErr instanceof Error ? sendErr.message : String(sendErr);
|
|
3272
|
+
this.logger.error({
|
|
3273
|
+
msg: `❌ [${service.name}] 发送错误响应失败`,
|
|
3274
|
+
error: sendErrMsg
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
handleServiceWithWait(res, mapping, fullUrl, startTime, method, headers) {
|
|
3280
|
+
const service = mapping.service;
|
|
3281
|
+
this.logger.info({
|
|
3282
|
+
msg: `⏳ [${service.name}] ${method} ${fullUrl} - 等待服务停止完成...`
|
|
3283
|
+
});
|
|
2805
3284
|
const chunks = [];
|
|
2806
3285
|
let aborted = false;
|
|
2807
3286
|
res.onAborted(()=>{
|
|
@@ -2813,46 +3292,35 @@ var __webpack_exports__ = {};
|
|
|
2813
3292
|
chunks.push(chunk);
|
|
2814
3293
|
if (isLast) {
|
|
2815
3294
|
const fullBody = Buffer.concat(chunks);
|
|
3295
|
+
if (aborted) return;
|
|
2816
3296
|
(async ()=>{
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
if (!isReady) {
|
|
2833
|
-
service._state.status = 'offline';
|
|
2834
|
-
throw new Error(`服务启动超时: 端口 ${service.base} 不可用`);
|
|
3297
|
+
const maxWaitTime = 30000;
|
|
3298
|
+
const checkInterval = 100;
|
|
3299
|
+
const waitStartTime = Date.now();
|
|
3300
|
+
while('stopping' === service._state.status){
|
|
3301
|
+
if (Date.now() - waitStartTime > maxWaitTime) {
|
|
3302
|
+
this.logger.error({
|
|
3303
|
+
msg: `❌ [${service.name}] 等待服务停止超时`
|
|
3304
|
+
});
|
|
3305
|
+
res.cork(()=>{
|
|
3306
|
+
res.writeStatus('503 Service Unavailable');
|
|
3307
|
+
res.end('Service stopping timeout');
|
|
3308
|
+
});
|
|
3309
|
+
return;
|
|
2835
3310
|
}
|
|
2836
|
-
|
|
2837
|
-
if (aborted) return;
|
|
2838
|
-
await this.forwardProxyRequest(res, service, fullUrl, startTime, method, headers, fullBody);
|
|
2839
|
-
} catch (error) {
|
|
2840
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2841
|
-
if ('Client aborted' === message) return;
|
|
2842
|
-
this.logger.error({
|
|
2843
|
-
msg: `❌ [${service.name}] 启动失败`,
|
|
2844
|
-
error: message
|
|
2845
|
-
});
|
|
2846
|
-
if (!aborted) res.cork(()=>{
|
|
2847
|
-
res.writeStatus('503 Service Unavailable');
|
|
2848
|
-
res.end('Service Unavailable');
|
|
2849
|
-
});
|
|
3311
|
+
await new Promise((resolve)=>setTimeout(resolve, checkInterval));
|
|
2850
3312
|
}
|
|
3313
|
+
if (aborted) return;
|
|
3314
|
+
this.logger.info({
|
|
3315
|
+
msg: `✅ [${service.name}] 服务已停止,开始启动...`
|
|
3316
|
+
});
|
|
3317
|
+
await this.startServiceAndProxy(res, mapping, fullUrl, startTime, method, headers, fullBody);
|
|
2851
3318
|
})();
|
|
2852
3319
|
}
|
|
2853
3320
|
});
|
|
2854
3321
|
}
|
|
2855
|
-
|
|
3322
|
+
handleServiceStart(res, mapping, fullUrl, startTime, method, headers) {
|
|
3323
|
+
mapping.service;
|
|
2856
3324
|
const chunks = [];
|
|
2857
3325
|
let aborted = false;
|
|
2858
3326
|
res.onAborted(()=>{
|
|
@@ -2865,74 +3333,108 @@ var __webpack_exports__ = {};
|
|
|
2865
3333
|
if (isLast) {
|
|
2866
3334
|
const fullBody = Buffer.concat(chunks);
|
|
2867
3335
|
if (aborted) return;
|
|
2868
|
-
this.
|
|
3336
|
+
this.startServiceAndProxy(res, mapping, fullUrl, startTime, method, headers, fullBody);
|
|
3337
|
+
}
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
handleDirectProxy(res, mapping, fullUrl, startTime, method, headers) {
|
|
3341
|
+
const service = mapping.service;
|
|
3342
|
+
const chunks = [];
|
|
3343
|
+
let aborted = false;
|
|
3344
|
+
res.onAborted(()=>{
|
|
3345
|
+
aborted = true;
|
|
3346
|
+
});
|
|
3347
|
+
res.onData((ab, isLast)=>{
|
|
3348
|
+
if (aborted) return;
|
|
3349
|
+
const chunk = Buffer.from(ab);
|
|
3350
|
+
chunks.push(chunk);
|
|
3351
|
+
if (isLast) {
|
|
3352
|
+
const fullBody = 1 === chunks.length ? chunks[0] : Buffer.concat(chunks);
|
|
3353
|
+
if (aborted) return;
|
|
3354
|
+
this.forwardProxyRequest(res, mapping, fullUrl, startTime, method, headers, fullBody).catch((err)=>{
|
|
2869
3355
|
if ('Client aborted' === err.message) return;
|
|
2870
3356
|
this.logger.error({
|
|
2871
3357
|
msg: `❌ [${service.name}] 代理失败`,
|
|
2872
3358
|
error: err.message
|
|
2873
3359
|
});
|
|
2874
|
-
if (!aborted)
|
|
2875
|
-
res.
|
|
2876
|
-
|
|
2877
|
-
|
|
3360
|
+
if (!aborted) try {
|
|
3361
|
+
res.cork(()=>{
|
|
3362
|
+
res.writeStatus('500 Internal Server Error');
|
|
3363
|
+
res.end('Proxy Error');
|
|
3364
|
+
});
|
|
3365
|
+
} catch (sendErr) {
|
|
3366
|
+
const sendErrMsg = sendErr instanceof Error ? sendErr.message : String(sendErr);
|
|
3367
|
+
this.logger.error({
|
|
3368
|
+
msg: `❌ [${service.name}] 发送错误响应失败`,
|
|
3369
|
+
error: sendErrMsg
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
2878
3372
|
});
|
|
2879
3373
|
}
|
|
2880
3374
|
});
|
|
2881
3375
|
}
|
|
2882
|
-
async forwardProxyRequest(res,
|
|
2883
|
-
const
|
|
2884
|
-
const
|
|
2885
|
-
const
|
|
2886
|
-
const
|
|
2887
|
-
|
|
2888
|
-
};
|
|
2889
|
-
|
|
2890
|
-
delete proxyHeaders['keep-alive'];
|
|
3376
|
+
async forwardProxyRequest(res, mapping, path, startTime, method, headers, body) {
|
|
3377
|
+
const service = mapping.service;
|
|
3378
|
+
const perfLog = this.logging.enablePerformanceLog;
|
|
3379
|
+
const perfPrepStart = perfLog ? performance.now() : 0;
|
|
3380
|
+
const targetUrl = mapping.targetUrl;
|
|
3381
|
+
const perfUrlTime = perfLog ? performance.now() - perfPrepStart : 0;
|
|
3382
|
+
const proxyHeaders = {};
|
|
3383
|
+
for(const key in headers)if ('connection' !== key && 'keep-alive' !== key) proxyHeaders[key] = headers[key];
|
|
2891
3384
|
proxyHeaders['host'] = targetUrl.host;
|
|
3385
|
+
const perfHeadersTime = perfLog ? performance.now() - perfPrepStart - perfUrlTime : 0;
|
|
2892
3386
|
const state = {
|
|
2893
3387
|
aborted: false,
|
|
2894
3388
|
responded: false
|
|
2895
3389
|
};
|
|
2896
3390
|
service._state.activeConnections++;
|
|
3391
|
+
const perfPrepTime = perfLog ? performance.now() - perfPrepStart : 0;
|
|
2897
3392
|
return new Promise((resolve, reject)=>{
|
|
3393
|
+
let cleaned = false;
|
|
2898
3394
|
const cleanup = ()=>{
|
|
2899
|
-
|
|
3395
|
+
if (!cleaned) {
|
|
3396
|
+
cleaned = true;
|
|
3397
|
+
service._state.activeConnections--;
|
|
3398
|
+
}
|
|
2900
3399
|
};
|
|
2901
3400
|
res.onAborted(()=>{
|
|
2902
3401
|
state.aborted = true;
|
|
2903
|
-
if (state.proxyReq && !state.proxyReq.destroyed) state.proxyReq.destroy();
|
|
2904
|
-
if (state.proxyRes && !state.proxyRes.destroyed) state.proxyRes.destroy();
|
|
2905
3402
|
cleanup();
|
|
2906
3403
|
resolve();
|
|
2907
3404
|
});
|
|
2908
|
-
|
|
3405
|
+
const perfHttpStart = perfLog ? performance.now() : 0;
|
|
3406
|
+
let perfTtfb = 0;
|
|
3407
|
+
let perfStreamStart = 0;
|
|
3408
|
+
let chunkCount = 0;
|
|
3409
|
+
let totalBytes = 0;
|
|
3410
|
+
const undiciClient = mapping.undiciClient;
|
|
3411
|
+
undiciClient.request({
|
|
3412
|
+
path,
|
|
2909
3413
|
method,
|
|
2910
3414
|
headers: proxyHeaders,
|
|
2911
|
-
|
|
2912
|
-
},
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
3415
|
+
body
|
|
3416
|
+
}).then(({ statusCode, headers, body })=>{
|
|
3417
|
+
if (perfLog && 0 === perfTtfb) {
|
|
3418
|
+
perfTtfb = performance.now() - perfHttpStart;
|
|
3419
|
+
perfStreamStart = performance.now();
|
|
3420
|
+
}
|
|
3421
|
+
const statusMessage = 'OK';
|
|
2916
3422
|
if (state.aborted) {
|
|
2917
|
-
|
|
3423
|
+
body.destroy();
|
|
2918
3424
|
cleanup();
|
|
2919
3425
|
resolve();
|
|
2920
3426
|
return;
|
|
2921
3427
|
}
|
|
2922
3428
|
if (101 === statusCode) {
|
|
2923
|
-
this.logger.info({
|
|
2924
|
-
msg: `✅ [${service.name}] WebSocket 升级成功`
|
|
2925
|
-
});
|
|
2926
3429
|
res.cork(()=>{
|
|
2927
3430
|
if (state.aborted) return;
|
|
2928
3431
|
res.writeStatus(`${statusCode} ${statusMessage}`);
|
|
2929
|
-
const
|
|
2930
|
-
for (const [key, value] of Object.entries(responseHeaders)){
|
|
3432
|
+
for(const key in headers){
|
|
2931
3433
|
const keyLower = key.toLowerCase();
|
|
2932
|
-
if ('connection'
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3434
|
+
if ('connection' === keyLower || 'transfer-encoding' === keyLower || 'keep-alive' === keyLower) continue;
|
|
3435
|
+
const value = headers[key];
|
|
3436
|
+
if (Array.isArray(value)) for (const v of value)res.writeHeader(key, v);
|
|
3437
|
+
else if (void 0 !== value) res.writeHeader(key, value);
|
|
2936
3438
|
}
|
|
2937
3439
|
res.end();
|
|
2938
3440
|
state.responded = true;
|
|
@@ -2944,61 +3446,82 @@ var __webpack_exports__ = {};
|
|
|
2944
3446
|
res.cork(()=>{
|
|
2945
3447
|
if (state.aborted) return;
|
|
2946
3448
|
res.writeStatus(`${statusCode} ${statusMessage}`);
|
|
2947
|
-
const
|
|
2948
|
-
for (const [key, value] of Object.entries(responseHeaders)){
|
|
3449
|
+
for(const key in headers){
|
|
2949
3450
|
const keyLower = key.toLowerCase();
|
|
2950
|
-
if ('connection'
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3451
|
+
if ('connection' === keyLower || 'transfer-encoding' === keyLower || 'keep-alive' === keyLower) continue;
|
|
3452
|
+
const value = headers[key];
|
|
3453
|
+
if (Array.isArray(value)) for (const v of value)res.writeHeader(key, v);
|
|
3454
|
+
else if (void 0 !== value) res.writeHeader(key, value);
|
|
2954
3455
|
}
|
|
2955
3456
|
});
|
|
2956
|
-
|
|
3457
|
+
body.on('data', (chunk)=>{
|
|
2957
3458
|
if (state.aborted) {
|
|
2958
|
-
|
|
3459
|
+
body.destroy();
|
|
2959
3460
|
return;
|
|
2960
3461
|
}
|
|
3462
|
+
if (perfLog) {
|
|
3463
|
+
chunkCount++;
|
|
3464
|
+
totalBytes += chunk.length;
|
|
3465
|
+
}
|
|
2961
3466
|
let writeSuccess = false;
|
|
2962
3467
|
res.cork(()=>{
|
|
2963
3468
|
if (state.aborted) return;
|
|
2964
3469
|
writeSuccess = res.write(chunk);
|
|
2965
3470
|
});
|
|
2966
3471
|
if (!writeSuccess) {
|
|
2967
|
-
|
|
3472
|
+
body.pause();
|
|
2968
3473
|
res.onWritable(()=>{
|
|
2969
3474
|
if (state.aborted) {
|
|
2970
|
-
|
|
3475
|
+
body.destroy();
|
|
2971
3476
|
return false;
|
|
2972
3477
|
}
|
|
2973
|
-
|
|
3478
|
+
body.resume();
|
|
2974
3479
|
return true;
|
|
2975
3480
|
});
|
|
2976
3481
|
}
|
|
2977
3482
|
});
|
|
2978
|
-
|
|
3483
|
+
body.on('end', ()=>{
|
|
2979
3484
|
if (state.aborted) {
|
|
2980
3485
|
cleanup();
|
|
2981
3486
|
resolve();
|
|
2982
3487
|
return;
|
|
2983
3488
|
}
|
|
3489
|
+
const perfStreamTime = perfLog ? performance.now() - perfStreamStart : 0;
|
|
3490
|
+
const perfTotalTime = perfLog ? performance.now() - perfPrepStart : 0;
|
|
2984
3491
|
res.cork(()=>{
|
|
2985
3492
|
if (state.aborted) return;
|
|
2986
3493
|
res.end();
|
|
2987
3494
|
state.responded = true;
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
3495
|
+
if (this.logging.enableRequestLog) {
|
|
3496
|
+
const responseTime = Date.now() - startTime;
|
|
3497
|
+
this.logger.info({
|
|
3498
|
+
msg: `📤 [${service.name}] ${method} ${path} - ${statusCode} - ${formatTime(responseTime)}`,
|
|
3499
|
+
service: service.name,
|
|
3500
|
+
method,
|
|
3501
|
+
path,
|
|
3502
|
+
statusCode,
|
|
3503
|
+
responseTime
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
if (perfLog) console.error(`⚡ [${service.name}] 性能分析:`, {
|
|
2992
3507
|
method,
|
|
2993
3508
|
path,
|
|
2994
3509
|
statusCode,
|
|
2995
|
-
|
|
3510
|
+
prepTime: perfPrepTime.toFixed(3) + 'ms',
|
|
3511
|
+
urlTime: perfUrlTime.toFixed(3) + 'ms',
|
|
3512
|
+
headersTime: perfHeadersTime.toFixed(3) + 'ms',
|
|
3513
|
+
ttfb: perfTtfb.toFixed(3) + 'ms',
|
|
3514
|
+
streamTime: perfStreamTime.toFixed(3) + 'ms',
|
|
3515
|
+
totalTime: perfTotalTime.toFixed(3) + 'ms',
|
|
3516
|
+
chunkCount,
|
|
3517
|
+
totalBytes,
|
|
3518
|
+
avgChunkSize: chunkCount > 0 ? (totalBytes / chunkCount).toFixed(1) + 'B' : '0B'
|
|
2996
3519
|
});
|
|
2997
3520
|
});
|
|
2998
3521
|
cleanup();
|
|
2999
3522
|
resolve();
|
|
3000
3523
|
});
|
|
3001
|
-
|
|
3524
|
+
body.on('error', (err)=>{
|
|
3002
3525
|
if (state.aborted) {
|
|
3003
3526
|
cleanup();
|
|
3004
3527
|
resolve();
|
|
@@ -3008,7 +3531,7 @@ var __webpack_exports__ = {};
|
|
|
3008
3531
|
msg: `❌ [${service.name}] 代理响应错误`,
|
|
3009
3532
|
error: err.message
|
|
3010
3533
|
});
|
|
3011
|
-
if (!state.responded) {
|
|
3534
|
+
if (!state.responded && !state.aborted) {
|
|
3012
3535
|
state.responded = true;
|
|
3013
3536
|
try {
|
|
3014
3537
|
res.cork(()=>{
|
|
@@ -3017,13 +3540,18 @@ var __webpack_exports__ = {};
|
|
|
3017
3540
|
res.end('Bad Gateway');
|
|
3018
3541
|
}
|
|
3019
3542
|
});
|
|
3020
|
-
} catch
|
|
3543
|
+
} catch (sendErr) {
|
|
3544
|
+
const sendErrMsg = sendErr instanceof Error ? sendErr.message : String(sendErr);
|
|
3545
|
+
this.logger.error({
|
|
3546
|
+
msg: `❌ [${service.name}] 发送错误响应失败`,
|
|
3547
|
+
error: sendErrMsg
|
|
3548
|
+
});
|
|
3549
|
+
}
|
|
3021
3550
|
}
|
|
3022
3551
|
cleanup();
|
|
3023
3552
|
reject(err);
|
|
3024
3553
|
});
|
|
3025
|
-
})
|
|
3026
|
-
state.proxyReq.on('error', (err)=>{
|
|
3554
|
+
}).catch((err)=>{
|
|
3027
3555
|
if (state.aborted) {
|
|
3028
3556
|
cleanup();
|
|
3029
3557
|
resolve();
|
|
@@ -3033,7 +3561,7 @@ var __webpack_exports__ = {};
|
|
|
3033
3561
|
msg: `❌ [${service.name}] 代理请求错误`,
|
|
3034
3562
|
error: err.message
|
|
3035
3563
|
});
|
|
3036
|
-
if (!state.responded) {
|
|
3564
|
+
if (!state.responded && !state.aborted) {
|
|
3037
3565
|
state.responded = true;
|
|
3038
3566
|
try {
|
|
3039
3567
|
res.cork(()=>{
|
|
@@ -3042,45 +3570,60 @@ var __webpack_exports__ = {};
|
|
|
3042
3570
|
res.end('Bad Gateway');
|
|
3043
3571
|
}
|
|
3044
3572
|
});
|
|
3045
|
-
} catch
|
|
3573
|
+
} catch (sendErr) {
|
|
3574
|
+
const sendErrMsg = sendErr instanceof Error ? sendErr.message : String(sendErr);
|
|
3575
|
+
this.logger.error({
|
|
3576
|
+
msg: `❌ [${service.name}] 发送错误响应失败`,
|
|
3577
|
+
error: sendErrMsg
|
|
3578
|
+
});
|
|
3579
|
+
}
|
|
3046
3580
|
}
|
|
3047
3581
|
cleanup();
|
|
3048
3582
|
reject(err);
|
|
3049
3583
|
});
|
|
3050
|
-
state.proxyReq.write(body);
|
|
3051
|
-
state.proxyReq.end();
|
|
3052
3584
|
});
|
|
3053
3585
|
}
|
|
3054
3586
|
async start() {
|
|
3055
|
-
const uWS = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "uWebSockets.js"));
|
|
3587
|
+
const uWS = await Promise.resolve().then(__webpack_require__.t.bind(__webpack_require__, "uWebSockets.js", 23));
|
|
3056
3588
|
const host = this.config.host || '127.0.0.1';
|
|
3057
3589
|
const port = this.config.port || 3000;
|
|
3058
3590
|
const app = uWS.App();
|
|
3059
3591
|
app.ws('/*', {
|
|
3060
3592
|
upgrade: (res, req, context)=>{
|
|
3061
3593
|
const hostname = req.getHeader('host')?.split(':')[0] || '';
|
|
3062
|
-
const
|
|
3063
|
-
if (!
|
|
3594
|
+
const mapping = this.hostnameRoutes.get(hostname);
|
|
3595
|
+
if (!mapping) {
|
|
3064
3596
|
res.cork(()=>{
|
|
3065
3597
|
res.writeStatus('404 Not Found');
|
|
3066
3598
|
res.end(`Service not found: ${hostname}`);
|
|
3067
3599
|
});
|
|
3068
3600
|
return;
|
|
3069
3601
|
}
|
|
3602
|
+
const { service, target } = mapping;
|
|
3070
3603
|
service._state.lastAccessTime = Date.now();
|
|
3604
|
+
const clientHeaders = {};
|
|
3605
|
+
req.forEach((key, value)=>{
|
|
3606
|
+
const safeValue = value.replace(/[\r\n]/g, '');
|
|
3607
|
+
clientHeaders[key] = safeValue;
|
|
3608
|
+
});
|
|
3609
|
+
const clientPath = req.getUrl() + (req.getQuery() ? `?${req.getQuery()}` : '');
|
|
3071
3610
|
res.upgrade({
|
|
3072
3611
|
hostname,
|
|
3073
|
-
service
|
|
3612
|
+
service,
|
|
3613
|
+
target,
|
|
3614
|
+
clientHeaders,
|
|
3615
|
+
clientPath
|
|
3074
3616
|
}, req.getHeader('sec-websocket-key'), req.getHeader('sec-websocket-protocol'), req.getHeader('sec-websocket-extensions'), context);
|
|
3075
|
-
this.logger.info({
|
|
3076
|
-
msg: `🔌 [${service.name}] WebSocket
|
|
3617
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3618
|
+
msg: `🔌 [${service.name}] WebSocket 升级请求: ${clientPath}`
|
|
3077
3619
|
});
|
|
3078
3620
|
},
|
|
3079
3621
|
open: (ws)=>{
|
|
3080
3622
|
const userData = ws.getUserData();
|
|
3081
3623
|
const service = userData.service;
|
|
3624
|
+
const target = userData.target;
|
|
3082
3625
|
service._state.activeConnections++;
|
|
3083
|
-
this.logger.info({
|
|
3626
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3084
3627
|
msg: `🔌 [${service.name}] WebSocket 连接已建立`
|
|
3085
3628
|
});
|
|
3086
3629
|
const wsState = {
|
|
@@ -3102,7 +3645,7 @@ var __webpack_exports__ = {};
|
|
|
3102
3645
|
const waitStartTime = Date.now();
|
|
3103
3646
|
let isReady = false;
|
|
3104
3647
|
while(Date.now() - waitStartTime < service.startTimeout){
|
|
3105
|
-
isReady = await
|
|
3648
|
+
isReady = await gateway_checkTcpPort(target);
|
|
3106
3649
|
if (isReady) {
|
|
3107
3650
|
const waitDuration = Date.now() - waitStartTime;
|
|
3108
3651
|
this.logger.info({
|
|
@@ -3121,30 +3664,47 @@ var __webpack_exports__ = {};
|
|
|
3121
3664
|
return;
|
|
3122
3665
|
}
|
|
3123
3666
|
service._state.status = 'online';
|
|
3667
|
+
service._state.startTime = Date.now();
|
|
3668
|
+
service._state.startCount++;
|
|
3124
3669
|
}
|
|
3125
|
-
const targetUrl = new
|
|
3126
|
-
const
|
|
3127
|
-
|
|
3670
|
+
const targetUrl = new external_node_url_.URL(target);
|
|
3671
|
+
const userData = ws.getUserData();
|
|
3672
|
+
const clientPath = userData.clientPath;
|
|
3673
|
+
const clientHeaders = userData.clientHeaders;
|
|
3674
|
+
const wsUrl = `${'https:' === targetUrl.protocol ? 'wss:' : 'ws:'}//${targetUrl.host}${clientPath}`;
|
|
3675
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3128
3676
|
msg: `🔌 [${service.name}] 连接后端 WebSocket: ${wsUrl}`
|
|
3129
3677
|
});
|
|
3678
|
+
const backendHeaders = {};
|
|
3679
|
+
const skipHeaders = new Set([
|
|
3680
|
+
'host',
|
|
3681
|
+
'connection',
|
|
3682
|
+
'upgrade',
|
|
3683
|
+
'sec-websocket-key',
|
|
3684
|
+
'sec-websocket-version'
|
|
3685
|
+
]);
|
|
3686
|
+
for (const [key, value] of Object.entries(clientHeaders))if (!skipHeaders.has(key.toLowerCase())) backendHeaders[key] = value;
|
|
3687
|
+
backendHeaders['Host'] = targetUrl.host;
|
|
3688
|
+
this.logger.info({
|
|
3689
|
+
msg: `🔌 [${service.name}] 转发 WebSocket 请求头`,
|
|
3690
|
+
headers: JSON.stringify(backendHeaders, null, 2)
|
|
3691
|
+
});
|
|
3130
3692
|
const backendWs = new wrapper(wsUrl, {
|
|
3131
|
-
headers:
|
|
3132
|
-
Host: targetUrl.host
|
|
3133
|
-
}
|
|
3693
|
+
headers: backendHeaders
|
|
3134
3694
|
});
|
|
3135
3695
|
wsState.backendWs = backendWs;
|
|
3136
3696
|
backendWs.on('open', ()=>{
|
|
3137
|
-
this.logger.info({
|
|
3697
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3138
3698
|
msg: `✅ [${service.name}] 后端 WebSocket 连接已建立`
|
|
3139
3699
|
});
|
|
3140
3700
|
wsState.backendReady = true;
|
|
3141
|
-
this.logger.info({
|
|
3701
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3142
3702
|
msg: `📤 [${service.name}] 发送队列中的 ${wsState.messageQueue.length} 条消息`
|
|
3143
3703
|
});
|
|
3144
3704
|
while(wsState.messageQueue.length > 0 && backendWs.readyState === wrapper.OPEN){
|
|
3145
3705
|
const msg = wsState.messageQueue.shift();
|
|
3146
3706
|
if (msg) {
|
|
3147
|
-
this.logger.info({
|
|
3707
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3148
3708
|
msg: `📨 [${service.name}] 发送队列消息: ${msg.length} 字节`
|
|
3149
3709
|
});
|
|
3150
3710
|
backendWs.send(msg);
|
|
@@ -3169,7 +3729,7 @@ var __webpack_exports__ = {};
|
|
|
3169
3729
|
}
|
|
3170
3730
|
});
|
|
3171
3731
|
backendWs.on('close', ()=>{
|
|
3172
|
-
this.logger.info({
|
|
3732
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3173
3733
|
msg: `🔌 [${service.name}] 后端 WebSocket 连接关闭`
|
|
3174
3734
|
});
|
|
3175
3735
|
if (null !== ws && !wsState.closing) {
|
|
@@ -3182,15 +3742,16 @@ var __webpack_exports__ = {};
|
|
|
3182
3742
|
msg: `❌ [${service.name}] 后端 WebSocket 错误`,
|
|
3183
3743
|
error: err.message
|
|
3184
3744
|
});
|
|
3745
|
+
wsState.closing = true;
|
|
3185
3746
|
if (null !== ws) ws.close();
|
|
3186
3747
|
});
|
|
3187
3748
|
backendWs.on('pause', ()=>{
|
|
3188
|
-
this.logger.info({
|
|
3749
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3189
3750
|
msg: `⏸️ [${service.name}] 后端 WebSocket 暂停(背压)`
|
|
3190
3751
|
});
|
|
3191
3752
|
});
|
|
3192
3753
|
backendWs.on('resume', ()=>{
|
|
3193
|
-
this.logger.info({
|
|
3754
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3194
3755
|
msg: `▶️ [${service.name}] 后端 WebSocket 恢复`
|
|
3195
3756
|
});
|
|
3196
3757
|
});
|
|
@@ -3200,7 +3761,7 @@ var __webpack_exports__ = {};
|
|
|
3200
3761
|
msg: `❌ [${service.name}] WebSocket 连接失败`,
|
|
3201
3762
|
error: message
|
|
3202
3763
|
});
|
|
3203
|
-
ws.close();
|
|
3764
|
+
if (null !== ws) ws.close();
|
|
3204
3765
|
}
|
|
3205
3766
|
})();
|
|
3206
3767
|
},
|
|
@@ -3210,13 +3771,13 @@ var __webpack_exports__ = {};
|
|
|
3210
3771
|
const wsState = ws.wsState;
|
|
3211
3772
|
if (wsState.backendReady && wsState.backendWs && wsState.backendWs.readyState === wrapper.OPEN) {
|
|
3212
3773
|
const msgBuffer = Buffer.from(message);
|
|
3213
|
-
this.logger.info({
|
|
3774
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3214
3775
|
msg: `📨 [${service.name}] 转发消息到后端: ${msgBuffer.length} 字节`
|
|
3215
3776
|
});
|
|
3216
3777
|
wsState.backendWs.send(msgBuffer);
|
|
3217
3778
|
service._state.lastAccessTime = Date.now();
|
|
3218
3779
|
} else {
|
|
3219
|
-
this.logger.info({
|
|
3780
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3220
3781
|
msg: `📦 [${service.name}] 消息加入队列`
|
|
3221
3782
|
});
|
|
3222
3783
|
wsState.messageQueue.push(Buffer.from(message));
|
|
@@ -3226,7 +3787,7 @@ var __webpack_exports__ = {};
|
|
|
3226
3787
|
const userData = ws.getUserData();
|
|
3227
3788
|
const service = userData.service;
|
|
3228
3789
|
service._state.activeConnections--;
|
|
3229
|
-
this.logger.info({
|
|
3790
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3230
3791
|
msg: `🔌 [${service.name}] 客户端 WebSocket 连接关闭`
|
|
3231
3792
|
});
|
|
3232
3793
|
const wsState = ws.wsState;
|
|
@@ -3247,6 +3808,195 @@ var __webpack_exports__ = {};
|
|
|
3247
3808
|
msg: `❌ DynaPM 网关启动失败: ${host}:${port}`
|
|
3248
3809
|
});
|
|
3249
3810
|
});
|
|
3811
|
+
for (const [portNum, mapping] of this.portRoutes)this.createPortBindingListener(host, portNum, mapping);
|
|
3812
|
+
const adminApiConfig = this.config.adminApi;
|
|
3813
|
+
if (adminApiConfig && false !== adminApiConfig.enabled && adminApiConfig.port) this.createAdminApiListener(host, adminApiConfig.port);
|
|
3814
|
+
}
|
|
3815
|
+
createAdminApiListener(host, port) {
|
|
3816
|
+
const app = external_uWebSockets_js_default().App();
|
|
3817
|
+
app.any('/*', (res, req)=>{
|
|
3818
|
+
this.adminApi.handleAdminApi(res, req);
|
|
3819
|
+
});
|
|
3820
|
+
app.listen(host, port, (token)=>{
|
|
3821
|
+
if (token) this.logger.info({
|
|
3822
|
+
msg: `🔌 管理 API 已启动: http://${host}:${port}`
|
|
3823
|
+
});
|
|
3824
|
+
else this.logger.error({
|
|
3825
|
+
msg: `❌ 管理 API 启动失败: ${host}:${port}`
|
|
3826
|
+
});
|
|
3827
|
+
});
|
|
3828
|
+
}
|
|
3829
|
+
createPortBindingListener(host, portNum, mapping) {
|
|
3830
|
+
const { service, target } = mapping;
|
|
3831
|
+
const app = external_uWebSockets_js_default().App();
|
|
3832
|
+
app.ws('/*', {
|
|
3833
|
+
upgrade: (res, req, context)=>{
|
|
3834
|
+
service._state.lastAccessTime = Date.now();
|
|
3835
|
+
const clientHeaders = {};
|
|
3836
|
+
req.forEach((key, value)=>{
|
|
3837
|
+
const safeValue = value.replace(/[\r\n]/g, '');
|
|
3838
|
+
clientHeaders[key] = safeValue;
|
|
3839
|
+
});
|
|
3840
|
+
const clientPath = req.getUrl() + (req.getQuery() ? `?${req.getQuery()}` : '');
|
|
3841
|
+
res.upgrade({
|
|
3842
|
+
service,
|
|
3843
|
+
target,
|
|
3844
|
+
clientHeaders,
|
|
3845
|
+
clientPath
|
|
3846
|
+
}, req.getHeader('sec-websocket-key'), req.getHeader('sec-websocket-protocol'), req.getHeader('sec-websocket-extensions'), context);
|
|
3847
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3848
|
+
msg: `🔌 [${service.name}] 端口${portNum} WebSocket 升级请求: ${clientPath}`
|
|
3849
|
+
});
|
|
3850
|
+
},
|
|
3851
|
+
open: (ws)=>{
|
|
3852
|
+
const userData = ws.getUserData();
|
|
3853
|
+
const svc = userData.service;
|
|
3854
|
+
const backendTarget = userData.target;
|
|
3855
|
+
svc._state.activeConnections++;
|
|
3856
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3857
|
+
msg: `🔌 [${svc.name}] 端口${portNum} WebSocket 连接已建立`
|
|
3858
|
+
});
|
|
3859
|
+
const wsState = {
|
|
3860
|
+
backendReady: false,
|
|
3861
|
+
messageQueue: [],
|
|
3862
|
+
backendWs: void 0,
|
|
3863
|
+
closing: false
|
|
3864
|
+
};
|
|
3865
|
+
ws.wsState = wsState;
|
|
3866
|
+
(async ()=>{
|
|
3867
|
+
try {
|
|
3868
|
+
const needsStart = 'offline' === svc._state.status;
|
|
3869
|
+
if (needsStart) {
|
|
3870
|
+
this.logger.info({
|
|
3871
|
+
msg: `🚀 [${svc.name}] 端口${portNum} WebSocket - 启动服务...`
|
|
3872
|
+
});
|
|
3873
|
+
svc._state.status = 'starting';
|
|
3874
|
+
await this.serviceManager.start(svc);
|
|
3875
|
+
const waitStartTime = Date.now();
|
|
3876
|
+
let isReady = false;
|
|
3877
|
+
while(Date.now() - waitStartTime < svc.startTimeout){
|
|
3878
|
+
isReady = await gateway_checkTcpPort(backendTarget);
|
|
3879
|
+
if (isReady) {
|
|
3880
|
+
const waitDuration = Date.now() - waitStartTime;
|
|
3881
|
+
this.logger.info({
|
|
3882
|
+
msg: `✅ [${svc.name}] 端口${portNum} WebSocket 服务就绪 (等待${formatTime(waitDuration)})`
|
|
3883
|
+
});
|
|
3884
|
+
break;
|
|
3885
|
+
}
|
|
3886
|
+
await new Promise((resolve)=>setTimeout(resolve, GatewayConstants.BACKEND_READY_CHECK_DELAY));
|
|
3887
|
+
}
|
|
3888
|
+
if (!isReady) {
|
|
3889
|
+
svc._state.status = 'offline';
|
|
3890
|
+
this.logger.error({
|
|
3891
|
+
msg: `❌ [${svc.name}] 端口${portNum} WebSocket 服务启动超时`
|
|
3892
|
+
});
|
|
3893
|
+
ws.close();
|
|
3894
|
+
return;
|
|
3895
|
+
}
|
|
3896
|
+
svc._state.status = 'online';
|
|
3897
|
+
svc._state.startTime = Date.now();
|
|
3898
|
+
svc._state.startCount++;
|
|
3899
|
+
}
|
|
3900
|
+
const targetUrl = new external_node_url_.URL(backendTarget);
|
|
3901
|
+
const userData = ws.getUserData();
|
|
3902
|
+
const clientPath = userData.clientPath;
|
|
3903
|
+
const clientHeaders = userData.clientHeaders;
|
|
3904
|
+
const wsUrl = `${'https:' === targetUrl.protocol ? 'wss:' : 'ws:'}//${targetUrl.host}${clientPath}`;
|
|
3905
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3906
|
+
msg: `🔌 [${svc.name}] 端口${portNum} 连接后端 WebSocket: ${wsUrl}`
|
|
3907
|
+
});
|
|
3908
|
+
const backendHeaders = {};
|
|
3909
|
+
const skipHeaders = new Set([
|
|
3910
|
+
'host',
|
|
3911
|
+
'connection',
|
|
3912
|
+
'upgrade',
|
|
3913
|
+
'sec-websocket-key',
|
|
3914
|
+
'sec-websocket-version'
|
|
3915
|
+
]);
|
|
3916
|
+
for (const [key, value] of Object.entries(clientHeaders))if (!skipHeaders.has(key.toLowerCase())) backendHeaders[key] = value;
|
|
3917
|
+
backendHeaders['Host'] = targetUrl.host;
|
|
3918
|
+
const backendWs = new wrapper(wsUrl, {
|
|
3919
|
+
headers: backendHeaders
|
|
3920
|
+
});
|
|
3921
|
+
wsState.backendWs = backendWs;
|
|
3922
|
+
backendWs.on('open', ()=>{
|
|
3923
|
+
if (this.logging.enableWebSocketLog) this.logger.info({
|
|
3924
|
+
msg: `✅ [${svc.name}] 端口${portNum} 后端 WebSocket 连接已建立`
|
|
3925
|
+
});
|
|
3926
|
+
wsState.backendReady = true;
|
|
3927
|
+
while(wsState.messageQueue.length > 0 && backendWs.readyState === wrapper.OPEN){
|
|
3928
|
+
const msg = wsState.messageQueue.shift();
|
|
3929
|
+
if (msg) backendWs.send(msg);
|
|
3930
|
+
}
|
|
3931
|
+
});
|
|
3932
|
+
backendWs.on('message', (data, isBinary)=>{
|
|
3933
|
+
if (null !== ws) ws.send(data, isBinary, false);
|
|
3934
|
+
});
|
|
3935
|
+
backendWs.on('close', ()=>{
|
|
3936
|
+
if (null !== ws && !wsState.closing) {
|
|
3937
|
+
wsState.closing = true;
|
|
3938
|
+
ws.close();
|
|
3939
|
+
}
|
|
3940
|
+
});
|
|
3941
|
+
backendWs.on('error', ()=>{
|
|
3942
|
+
wsState.closing = true;
|
|
3943
|
+
if (null !== ws) ws.close();
|
|
3944
|
+
});
|
|
3945
|
+
} catch (error) {
|
|
3946
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3947
|
+
this.logger.error({
|
|
3948
|
+
msg: `❌ [${service.name}] WebSocket 连接失败`,
|
|
3949
|
+
error: message
|
|
3950
|
+
});
|
|
3951
|
+
if (null !== ws) ws.close();
|
|
3952
|
+
}
|
|
3953
|
+
})();
|
|
3954
|
+
},
|
|
3955
|
+
message: (ws, message, _isBinary)=>{
|
|
3956
|
+
const userData = ws.getUserData();
|
|
3957
|
+
const svc = userData.service;
|
|
3958
|
+
const wsState = ws.wsState;
|
|
3959
|
+
if (wsState.backendReady && wsState.backendWs && wsState.backendWs.readyState === wrapper.OPEN) {
|
|
3960
|
+
wsState.backendWs.send(Buffer.from(message));
|
|
3961
|
+
svc._state.lastAccessTime = Date.now();
|
|
3962
|
+
} else wsState.messageQueue.push(Buffer.from(message));
|
|
3963
|
+
},
|
|
3964
|
+
close: (ws)=>{
|
|
3965
|
+
const userData = ws.getUserData();
|
|
3966
|
+
const svc = userData.service;
|
|
3967
|
+
svc._state.activeConnections--;
|
|
3968
|
+
}
|
|
3969
|
+
});
|
|
3970
|
+
app.any('/*', (res, req)=>{
|
|
3971
|
+
this.handlePortBindingRequest(res, req, mapping);
|
|
3972
|
+
});
|
|
3973
|
+
app.listen(host, portNum, (token)=>{
|
|
3974
|
+
if (token) this.logger.info({
|
|
3975
|
+
msg: `🔌 端口绑定已启动: http://${host}:${portNum} -> ${service.name}`
|
|
3976
|
+
});
|
|
3977
|
+
else this.logger.error({
|
|
3978
|
+
msg: `❌ 端口绑定启动失败: ${host}:${portNum}`
|
|
3979
|
+
});
|
|
3980
|
+
});
|
|
3981
|
+
}
|
|
3982
|
+
async cleanup() {
|
|
3983
|
+
this.logger.info({
|
|
3984
|
+
msg: '🧹 正在清理所有服务...'
|
|
3985
|
+
});
|
|
3986
|
+
const cleanedServices = new Set();
|
|
3987
|
+
for (const mapping of this.hostnameRoutes.values())cleanedServices.add(mapping.service);
|
|
3988
|
+
for (const mapping of this.portRoutes.values())cleanedServices.add(mapping.service);
|
|
3989
|
+
const stopPromises = [];
|
|
3990
|
+
for (const service of cleanedServices)if ('online' === service._state.status || 'starting' === service._state.status) stopPromises.push(this.serviceManager.stop(service).catch((err)=>{
|
|
3991
|
+
this.logger.error({
|
|
3992
|
+
msg: `❌ [${service.name}] 停止失败`,
|
|
3993
|
+
error: err.message
|
|
3994
|
+
});
|
|
3995
|
+
}));
|
|
3996
|
+
await Promise.all(stopPromises);
|
|
3997
|
+
this.logger.info({
|
|
3998
|
+
msg: `✅ 已清理 ${cleanedServices.size} 个服务`
|
|
3999
|
+
});
|
|
3250
4000
|
}
|
|
3251
4001
|
}
|
|
3252
4002
|
const external_c12_namespaceObject = require("c12");
|
|
@@ -3256,18 +4006,65 @@ var __webpack_exports__ = {};
|
|
|
3256
4006
|
defaultConfig: {
|
|
3257
4007
|
port: 3000,
|
|
3258
4008
|
host: '127.0.0.1',
|
|
3259
|
-
services: {}
|
|
4009
|
+
services: {},
|
|
4010
|
+
adminApi: {
|
|
4011
|
+
enabled: true,
|
|
4012
|
+
port: 4000,
|
|
4013
|
+
allowedIps: [
|
|
4014
|
+
'127.0.0.1',
|
|
4015
|
+
'::1'
|
|
4016
|
+
]
|
|
4017
|
+
}
|
|
3260
4018
|
}
|
|
3261
4019
|
});
|
|
3262
4020
|
if (!config.services || 0 === Object.keys(config.services).length) throw new Error('配置文件中至少需要一个服务');
|
|
3263
|
-
|
|
3264
|
-
|
|
4021
|
+
const mainPort = config.port || 3000;
|
|
4022
|
+
if (config.adminApi?.enabled && config.adminApi.port === mainPort) throw new Error(`管理 API 端口 ${mainPort} 与主端口冲突`);
|
|
4023
|
+
const portMap = new Map();
|
|
4024
|
+
const hostnameMap = new Map();
|
|
4025
|
+
for (const [key, service] of Object.entries(config.services)){
|
|
4026
|
+
service.name = service.name || key;
|
|
3265
4027
|
service.idleTimeout = service.idleTimeout || 300000;
|
|
3266
4028
|
service.startTimeout = service.startTimeout || 30000;
|
|
3267
4029
|
service.healthCheck = service.healthCheck || {
|
|
3268
4030
|
type: 'tcp'
|
|
3269
4031
|
};
|
|
3270
4032
|
if ('http' === service.healthCheck.type && !service.healthCheck.url) service.healthCheck.url = service.base;
|
|
4033
|
+
let routes = service.routes;
|
|
4034
|
+
if (!routes || 0 === routes.length) {
|
|
4035
|
+
routes = [];
|
|
4036
|
+
if (service.host) routes.push({
|
|
4037
|
+
type: 'host',
|
|
4038
|
+
value: service.host,
|
|
4039
|
+
target: service.base
|
|
4040
|
+
});
|
|
4041
|
+
if (service.port) routes.push({
|
|
4042
|
+
type: 'port',
|
|
4043
|
+
value: service.port,
|
|
4044
|
+
target: service.base
|
|
4045
|
+
});
|
|
4046
|
+
if (0 === routes.length) routes.push({
|
|
4047
|
+
type: 'host',
|
|
4048
|
+
value: key,
|
|
4049
|
+
target: service.base
|
|
4050
|
+
});
|
|
4051
|
+
service.routes = routes;
|
|
4052
|
+
}
|
|
4053
|
+
for (const route of routes)if (!route.target) throw new Error(`服务 [${service.name}] 的路由配置缺少 target 字段`);
|
|
4054
|
+
for (const route of routes)if ('port' === route.type) {
|
|
4055
|
+
const port = route.value;
|
|
4056
|
+
if (port === mainPort) throw new Error(`服务 [${service.name}] 的路由端口 ${port} 与主端口冲突`);
|
|
4057
|
+
const adminApiPort = config.adminApi?.enabled ? config.adminApi.port : void 0;
|
|
4058
|
+
if (adminApiPort && port === adminApiPort) throw new Error(`服务 [${service.name}] 的路由端口 ${port} 与管理 API 端口冲突`);
|
|
4059
|
+
const existingService = portMap.get(port);
|
|
4060
|
+
if (existingService) throw new Error(`端口冲突: 服务 [${service.name}] 和 [${existingService}] 都配置了端口 ${port}`);
|
|
4061
|
+
portMap.set(port, service.name);
|
|
4062
|
+
} else if ('host' === route.type) {
|
|
4063
|
+
const hostname = route.value;
|
|
4064
|
+
const existingService = hostnameMap.get(hostname);
|
|
4065
|
+
if (existingService && existingService !== service.name) throw new Error(`Hostname 冲突: 服务 [${service.name}] 和 [${existingService}] 都配置了 hostname ${hostname}`);
|
|
4066
|
+
hostnameMap.set(hostname, service.name);
|
|
4067
|
+
}
|
|
3271
4068
|
}
|
|
3272
4069
|
return config;
|
|
3273
4070
|
}
|
|
@@ -3323,6 +4120,20 @@ var __webpack_exports__ = {};
|
|
|
3323
4120
|
msg: 'DynaPM 网关已启动',
|
|
3324
4121
|
port: config.port || 3000
|
|
3325
4122
|
});
|
|
4123
|
+
const cleanup = async (signal)=>{
|
|
4124
|
+
logger.info({
|
|
4125
|
+
msg: `⚠️ 收到 ${signal} 信号,正在清理...`
|
|
4126
|
+
});
|
|
4127
|
+
await gateway.cleanup();
|
|
4128
|
+
process.exit(0);
|
|
4129
|
+
};
|
|
4130
|
+
process.on('SIGINT', ()=>cleanup('SIGINT'));
|
|
4131
|
+
process.on('SIGTERM', ()=>cleanup('SIGTERM'));
|
|
4132
|
+
process.on('exit', ()=>{
|
|
4133
|
+
logger.info({
|
|
4134
|
+
msg: '👋 DynaPM 网关已退出'
|
|
4135
|
+
});
|
|
4136
|
+
});
|
|
3326
4137
|
} catch (error) {
|
|
3327
4138
|
logger.error({
|
|
3328
4139
|
msg: '启动失败',
|