koishi-plugin-docker-control 0.1.1 → 0.1.2
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/lib/commands/index.js +7 -4
- package/lib/service/connector.d.ts +9 -0
- package/lib/service/connector.js +31 -15
- package/lib/service/node.d.ts +16 -3
- package/lib/service/node.js +306 -101
- package/package.json +1 -1
package/lib/commands/index.js
CHANGED
|
@@ -133,18 +133,21 @@ function registerHelperCommands(ctx, getService, config) {
|
|
|
133
133
|
const html = (0, render_1.generateNodeDetailHtml)(nodeData, version, systemInfo);
|
|
134
134
|
return await (0, render_1.renderToImage)(ctx, html);
|
|
135
135
|
}
|
|
136
|
-
const memoryUsed = systemInfo?.MemTotal && systemInfo?.MemAvailable !== undefined
|
|
137
|
-
? `${Math.round((1 - systemInfo.MemAvailable / systemInfo.MemTotal) * 100)}%`
|
|
138
|
-
: '-';
|
|
139
136
|
const nodeName = node.config?.name || node.name || node.Name || 'Unknown';
|
|
140
137
|
const nodeId = node.id || node.ID || node.Id || node.config?.id || '-';
|
|
138
|
+
// 格式化内存显示:总内存量
|
|
139
|
+
let memoryDisplay = '-';
|
|
140
|
+
if (systemInfo?.MemTotal) {
|
|
141
|
+
const memGB = (systemInfo.MemTotal / 1024 / 1024 / 1024).toFixed(2);
|
|
142
|
+
memoryDisplay = `${memGB} GB`;
|
|
143
|
+
}
|
|
141
144
|
const lines = [
|
|
142
145
|
`=== ${nodeName} ===`,
|
|
143
146
|
`ID: ${nodeId}`,
|
|
144
147
|
`状态: ${node.status || node.Status || 'unknown'}`,
|
|
145
148
|
`标签: ${node.tags?.join(', ') || node.config?.tags?.join(', ') || '无'}`,
|
|
146
149
|
`CPU: ${systemInfo?.NCPU || '-'} 核心`,
|
|
147
|
-
`内存: ${
|
|
150
|
+
`内存: ${memoryDisplay}`,
|
|
148
151
|
`容器: ${containerCount.running}/${containerCount.total} 运行中`,
|
|
149
152
|
`镜像: ${imageCount} 个`,
|
|
150
153
|
`Docker 版本: ${version.Version}`,
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH 连接器 - 通过 SSH 执行 docker 命令
|
|
3
|
+
*/
|
|
4
|
+
import { Client } from 'ssh2';
|
|
1
5
|
import type { NodeConfig, DockerControlConfig } from '../types';
|
|
2
6
|
export declare class DockerConnector {
|
|
3
7
|
private config;
|
|
4
8
|
private fullConfig;
|
|
5
9
|
private sshClient;
|
|
6
10
|
constructor(config: NodeConfig, fullConfig: DockerControlConfig);
|
|
11
|
+
/**
|
|
12
|
+
* 获取内部 SSH Client(用于连接复用)
|
|
13
|
+
* 如果尚未连接,会触发连接建立
|
|
14
|
+
*/
|
|
15
|
+
getSshClient(): Promise<Client>;
|
|
7
16
|
/**
|
|
8
17
|
* 验证并修正配置
|
|
9
18
|
*/
|
package/lib/service/connector.js
CHANGED
|
@@ -17,6 +17,13 @@ class DockerConnector {
|
|
|
17
17
|
// 立即验证并修正配置
|
|
18
18
|
this.validateConfig();
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* 获取内部 SSH Client(用于连接复用)
|
|
22
|
+
* 如果尚未连接,会触发连接建立
|
|
23
|
+
*/
|
|
24
|
+
async getSshClient() {
|
|
25
|
+
return await this.getConnection();
|
|
26
|
+
}
|
|
20
27
|
/**
|
|
21
28
|
* 验证并修正配置
|
|
22
29
|
*/
|
|
@@ -55,7 +62,8 @@ class DockerConnector {
|
|
|
55
62
|
const msg = err.message || '';
|
|
56
63
|
// 如果是 SSH 通道打开失败,或者是连接已结束,则强制重连
|
|
57
64
|
if (msg.includes('Channel open failure') || msg.includes('Client ended') || msg.includes('Socket ended')) {
|
|
58
|
-
logger_1.connectorLogger.warn(`[${this.config.name}] SSH
|
|
65
|
+
logger_1.connectorLogger.warn(`[${this.config.name}] ⚠ SSH连接异常: ${msg},尝试重连...`);
|
|
66
|
+
logger_1.connectorLogger.debug(`[${this.config.name}] 重连将产生新的SSH登录记录`);
|
|
59
67
|
this.dispose(); // 强制销毁当前连接
|
|
60
68
|
continue; // 重试
|
|
61
69
|
}
|
|
@@ -67,11 +75,11 @@ class DockerConnector {
|
|
|
67
75
|
}
|
|
68
76
|
async execInternal(command) {
|
|
69
77
|
const client = await this.getConnection();
|
|
70
|
-
logger_1.connectorLogger.debug(`[${this.config.name}]
|
|
78
|
+
logger_1.connectorLogger.debug(`[${this.config.name}] 🔧 执行SSH命令: ${command}`);
|
|
71
79
|
return new Promise((resolve, reject) => {
|
|
72
80
|
client.exec(command, (err, stream) => {
|
|
73
81
|
if (err) {
|
|
74
|
-
logger_1.connectorLogger.
|
|
82
|
+
logger_1.connectorLogger.warn(`[${this.config.name}] SSH命令执行失败: ${err.message}`);
|
|
75
83
|
reject(err);
|
|
76
84
|
return;
|
|
77
85
|
}
|
|
@@ -196,7 +204,7 @@ class DockerConnector {
|
|
|
196
204
|
reject(err);
|
|
197
205
|
return;
|
|
198
206
|
}
|
|
199
|
-
logger_1.connectorLogger.info(`[${this.config.name}] Docker
|
|
207
|
+
logger_1.connectorLogger.info(`[${this.config.name}] ✅ Docker 事件流已建立长连接 (docker events --format json --filter type=container)`);
|
|
200
208
|
let buffer = '';
|
|
201
209
|
let closed = false;
|
|
202
210
|
const stop = () => {
|
|
@@ -211,13 +219,14 @@ class DockerConnector {
|
|
|
211
219
|
catch (e) {
|
|
212
220
|
// 可能已经关闭,忽略错误
|
|
213
221
|
}
|
|
214
|
-
logger_1.connectorLogger.
|
|
222
|
+
logger_1.connectorLogger.info(`[${this.config.name}] 🔒 主动停止事件流`);
|
|
215
223
|
}
|
|
216
224
|
};
|
|
217
225
|
stream.on('close', (code, signal) => {
|
|
218
226
|
if (!closed) {
|
|
219
227
|
closed = true;
|
|
220
|
-
logger_1.connectorLogger.
|
|
228
|
+
logger_1.connectorLogger.error(`[${this.config.name}] ❌ 事件流意外断开!Code: ${code}, Signal: ${signal}`);
|
|
229
|
+
logger_1.connectorLogger.error(`[${this.config.name}] ⚠ 事件流断开后,node.ts 会自动重连 (将产生新的SSH登录记录)`);
|
|
221
230
|
}
|
|
222
231
|
});
|
|
223
232
|
stream.on('data', (data) => {
|
|
@@ -251,6 +260,7 @@ class DockerConnector {
|
|
|
251
260
|
*/
|
|
252
261
|
dispose() {
|
|
253
262
|
if (this.sshClient) {
|
|
263
|
+
logger_1.connectorLogger.info(`[${this.config.name}] 主动销毁 SSH 连接`);
|
|
254
264
|
this.sshClient.end();
|
|
255
265
|
this.sshClient = null;
|
|
256
266
|
}
|
|
@@ -272,29 +282,32 @@ class DockerConnector {
|
|
|
272
282
|
if (!credential) {
|
|
273
283
|
throw new Error(`凭证不存在: ${this.config.credentialId}`);
|
|
274
284
|
}
|
|
275
|
-
|
|
276
|
-
|
|
285
|
+
const port = typeof this.config.port === 'string'
|
|
286
|
+
? parseInt(this.config.port, 10)
|
|
287
|
+
: (this.config.port || 22);
|
|
288
|
+
logger_1.connectorLogger.info(`[${this.config.name}] 🔗 建立新的SSH连接...`);
|
|
289
|
+
logger_1.connectorLogger.info(`[${this.config.name}] 目标: ${credential.username}@${this.config.host}:${port}`);
|
|
290
|
+
logger_1.connectorLogger.info(`[${this.config.name}] 认证方式: ${credential.authType}`);
|
|
277
291
|
return new Promise((resolve, reject) => {
|
|
278
292
|
const conn = new ssh2_1.Client();
|
|
279
293
|
conn.on('ready', () => {
|
|
280
|
-
logger_1.connectorLogger.info(`[${this.config.name}] SSH
|
|
294
|
+
logger_1.connectorLogger.info(`[${this.config.name}] ✅ SSH连接成功 (user=${credential.username}, host=${this.config.host}, port=${port})`);
|
|
281
295
|
resolve(conn);
|
|
282
296
|
});
|
|
283
297
|
conn.on('error', (err) => {
|
|
284
|
-
logger_1.connectorLogger.error(`[${this.config.name}] SSH
|
|
298
|
+
logger_1.connectorLogger.error(`[${this.config.name}] ❌ SSH连接失败: ${err.message} (host=${this.config.host}, port=${port})`);
|
|
299
|
+
logger_1.connectorLogger.error(`[${this.config.name}] ⚠ 连接失败后将在片刻重试 (重试会产生新的SSH登录记录)`);
|
|
285
300
|
conn.end();
|
|
286
301
|
reject(err);
|
|
287
302
|
});
|
|
288
303
|
conn.on('close', () => {
|
|
289
|
-
|
|
304
|
+
const reason = this.connected ? 'SSH连接意外断开' : 'SSH连接已关闭';
|
|
305
|
+
logger_1.connectorLogger.warn(`[${this.config.name}] ${reason} (host=${this.config.host}, port=${this.config.port})`);
|
|
306
|
+
this.connected = false;
|
|
290
307
|
});
|
|
291
308
|
conn.on('banner', (msg) => {
|
|
292
309
|
logger_1.connectorLogger.debug(`[${this.config.name}] SSH Banner: ${msg.trim()}`);
|
|
293
310
|
});
|
|
294
|
-
// 确保 port 是数字类型
|
|
295
|
-
const port = typeof this.config.port === 'string'
|
|
296
|
-
? parseInt(this.config.port, 10)
|
|
297
|
-
: (this.config.port || 22);
|
|
298
311
|
const connectConfig = {
|
|
299
312
|
host: this.config.host,
|
|
300
313
|
port: port,
|
|
@@ -302,6 +315,9 @@ class DockerConnector {
|
|
|
302
315
|
readyTimeout: constants_1.SSH_TIMEOUT,
|
|
303
316
|
timeout: constants_1.SSH_TIMEOUT,
|
|
304
317
|
tryKeyboard: true,
|
|
318
|
+
// === 保持连接活跃,防止被服务器踢掉 ===
|
|
319
|
+
keepaliveInterval: 15000, // 每15秒发送一次心跳
|
|
320
|
+
keepaliveCountMax: 3, // 失败3次认为断开
|
|
305
321
|
...this.buildAuthOptions(credential),
|
|
306
322
|
};
|
|
307
323
|
conn.connect(connectConfig);
|
package/lib/service/node.d.ts
CHANGED
|
@@ -10,8 +10,10 @@ export declare class DockerNode {
|
|
|
10
10
|
status: NodeStatusType;
|
|
11
11
|
/** Koishi Context (用于数据库操作) */
|
|
12
12
|
private readonly ctx;
|
|
13
|
-
/** SSH 连接器 */
|
|
13
|
+
/** SSH 连接器 (Fallback用) */
|
|
14
14
|
private connector;
|
|
15
|
+
/** 持久化 SSH 客户端 (API用) */
|
|
16
|
+
private sshClient;
|
|
15
17
|
/** Dockerode 实例 (用于 API 调用) */
|
|
16
18
|
private dockerode;
|
|
17
19
|
/** Docker API 是否可用 */
|
|
@@ -43,13 +45,17 @@ export declare class DockerNode {
|
|
|
43
45
|
constructor(ctx: Context, config: NodeConfig, credential: CredentialConfig, debug?: boolean);
|
|
44
46
|
/**
|
|
45
47
|
* 连接到 Docker (带重试)
|
|
46
|
-
*
|
|
48
|
+
* 优化:优先尝试 API 连接,成功则不再建立多余的 SSH 命令行连接
|
|
47
49
|
*/
|
|
48
50
|
connect(): Promise<void>;
|
|
49
51
|
/**
|
|
50
52
|
* 验证和清理配置
|
|
51
53
|
*/
|
|
52
54
|
private validateAndCleanConfig;
|
|
55
|
+
/**
|
|
56
|
+
* 销毁 SSH 客户端
|
|
57
|
+
*/
|
|
58
|
+
private disposeSshClient;
|
|
53
59
|
/**
|
|
54
60
|
* 断开连接
|
|
55
61
|
*/
|
|
@@ -79,6 +85,7 @@ export declare class DockerNode {
|
|
|
79
85
|
}>;
|
|
80
86
|
/**
|
|
81
87
|
* 获取系统信息 (CPU、内存)
|
|
88
|
+
* 优先使用 Docker API,失败时降级到 SSH 命令
|
|
82
89
|
*/
|
|
83
90
|
getSystemInfo(): Promise<{
|
|
84
91
|
NCPU: number;
|
|
@@ -264,7 +271,7 @@ export declare class DockerNode {
|
|
|
264
271
|
}>>;
|
|
265
272
|
/**
|
|
266
273
|
* 初始化 Dockerode
|
|
267
|
-
*
|
|
274
|
+
* 建立唯一的 SSH 连接,并通过 `docker system dial-stdio` 复用连接
|
|
268
275
|
*/
|
|
269
276
|
private initDockerode;
|
|
270
277
|
/**
|
|
@@ -329,6 +336,7 @@ export declare class DockerNode {
|
|
|
329
336
|
private startMonitoring;
|
|
330
337
|
/**
|
|
331
338
|
* 启动 API 健康检查
|
|
339
|
+
* DPanel模式:信任底层 Keep-Alive,不主动 Ping,只在操作报错时重连
|
|
332
340
|
*/
|
|
333
341
|
private startHealthCheck;
|
|
334
342
|
/**
|
|
@@ -349,8 +357,13 @@ export declare class DockerNode {
|
|
|
349
357
|
private pollContainerStates;
|
|
350
358
|
/**
|
|
351
359
|
* 启动 Docker 事件流监听
|
|
360
|
+
* 优先使用 Docker API (长连接且有心跳),失败降级到 SSH 命令
|
|
352
361
|
*/
|
|
353
362
|
private startEventStream;
|
|
363
|
+
/**
|
|
364
|
+
* 重启事件流
|
|
365
|
+
*/
|
|
366
|
+
private restartEventStream;
|
|
354
367
|
/**
|
|
355
368
|
* 处理事件流中的一行数据
|
|
356
369
|
*/
|
package/lib/service/node.js
CHANGED
|
@@ -9,6 +9,8 @@ exports.DockerNode = void 0;
|
|
|
9
9
|
*/
|
|
10
10
|
const koishi_1 = require("koishi");
|
|
11
11
|
const dockerode_1 = __importDefault(require("dockerode"));
|
|
12
|
+
const http_1 = __importDefault(require("http"));
|
|
13
|
+
const ssh2_1 = require("ssh2");
|
|
12
14
|
const constants_1 = require("../constants");
|
|
13
15
|
const connector_1 = require("./connector");
|
|
14
16
|
const logger_1 = require("../utils/logger");
|
|
@@ -18,8 +20,10 @@ class DockerNode {
|
|
|
18
20
|
constructor(ctx, config, credential, debug = false) {
|
|
19
21
|
/** 节点状态 */
|
|
20
22
|
this.status = constants_1.NodeStatus.DISCONNECTED;
|
|
21
|
-
/** SSH 连接器 */
|
|
23
|
+
/** SSH 连接器 (Fallback用) */
|
|
22
24
|
this.connector = null;
|
|
25
|
+
/** 持久化 SSH 客户端 (API用) */
|
|
26
|
+
this.sshClient = null;
|
|
23
27
|
/** Dockerode 实例 (用于 API 调用) */
|
|
24
28
|
this.dockerode = null;
|
|
25
29
|
/** Docker API 是否可用 */
|
|
@@ -67,7 +71,7 @@ class DockerNode {
|
|
|
67
71
|
}
|
|
68
72
|
/**
|
|
69
73
|
* 连接到 Docker (带重试)
|
|
70
|
-
*
|
|
74
|
+
* 优化:优先尝试 API 连接,成功则不再建立多余的 SSH 命令行连接
|
|
71
75
|
*/
|
|
72
76
|
async connect() {
|
|
73
77
|
if (this.status === constants_1.NodeStatus.CONNECTING) {
|
|
@@ -91,21 +95,33 @@ class DockerNode {
|
|
|
91
95
|
logger_1.nodeLogger.info(`[${this.name}] 连接尝试 ${attempt} (每 ${LONG_RETRY_INTERVAL / 1000} 秒重试)...`);
|
|
92
96
|
}
|
|
93
97
|
try {
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
+
// === 优化策略:完全依赖 Docker API,不预创建 connector ===
|
|
99
|
+
// 只有在 API 真正失败时,才创建 connector 并建立 SSH 连接
|
|
100
|
+
// 1. 先尝试初始化 Docker API(不创建 connector)
|
|
101
|
+
// 这可能会产生 1-2 个 SSH 连接(ping + getEvents)
|
|
98
102
|
await this.initDockerode();
|
|
99
|
-
//
|
|
103
|
+
// 2. 只有当 API 不可用时,才创建 connector 并降级到 SSH 命令
|
|
100
104
|
if (!this.dockerApiAvailable) {
|
|
101
|
-
logger_1.nodeLogger.
|
|
105
|
+
logger_1.nodeLogger.warn(`[${this.name}] Docker API 不可用,创建 connector 并降级到 SSH 命令...`);
|
|
106
|
+
const connector = new connector_1.DockerConnector(this.config, { credentials: [this.credential], nodes: [this.config] });
|
|
107
|
+
this.connector = connector;
|
|
108
|
+
// 测试 SSH 命令(这会建立第 1 个 SSH 连接)
|
|
102
109
|
await connector.exec('docker version --format "{{.Server.Version}}"');
|
|
110
|
+
logger_1.nodeLogger.info(`[${this.name}] ⚠ 已启用 SSH 命令模式`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// API 可用:创建一个懒加载的 connector(不立即连接)
|
|
114
|
+
// 只有当真正需要执行 SSH 命令时才建立连接
|
|
115
|
+
const connector = new connector_1.DockerConnector(this.config, { credentials: [this.credential], nodes: [this.config] });
|
|
116
|
+
this.connector = connector;
|
|
117
|
+
// 标记为 connected(但实际 SSH 连接尚未建立)
|
|
118
|
+
connector.setConnected(true);
|
|
119
|
+
logger_1.nodeLogger.info(`[${this.name}] ✅ Connector 已创建(懒加载模式,使用时才连接)`);
|
|
103
120
|
}
|
|
104
|
-
// 标记连接可用,允许事件流自动重连
|
|
105
|
-
connector.setConnected(true);
|
|
106
121
|
this.status = constants_1.NodeStatus.CONNECTED;
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
const mode = this.dockerApiAvailable ? 'Docker API (SSH隧道复用)' : 'SSH 命令模式';
|
|
123
|
+
logger_1.nodeLogger.info(`[${this.name}] 连接成功 [模式: ${mode}]`);
|
|
124
|
+
// 启动监控 (此时 API 已就绪,startEventStream 会复用 API 连接,不会产生新登录)
|
|
109
125
|
this.startMonitoring();
|
|
110
126
|
// 触发上线事件
|
|
111
127
|
this.emitEvent({
|
|
@@ -122,8 +138,10 @@ class DockerNode {
|
|
|
122
138
|
const lastError = error instanceof Error ? error : new Error(String(error));
|
|
123
139
|
logger_1.nodeLogger.warn(`[${this.name}] 连接失败: ${lastError.message}`);
|
|
124
140
|
// 清理连接
|
|
141
|
+
this.disposeSshClient();
|
|
125
142
|
this.connector?.dispose();
|
|
126
143
|
this.connector = null;
|
|
144
|
+
this.dockerode = null; // 确保清理
|
|
127
145
|
// 等待后重试
|
|
128
146
|
logger_1.nodeLogger.info(`[${this.name}] ${currentInterval / 1000} 秒后重试...`);
|
|
129
147
|
await new Promise(resolve => setTimeout(resolve, currentInterval));
|
|
@@ -165,12 +183,28 @@ class DockerNode {
|
|
|
165
183
|
logger_1.nodeLogger.info(`[${this.name}] 配置已修正: host="${this.config.host}", port=${cleanedPort}`);
|
|
166
184
|
}
|
|
167
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* 销毁 SSH 客户端
|
|
188
|
+
*/
|
|
189
|
+
disposeSshClient() {
|
|
190
|
+
if (this.sshClient) {
|
|
191
|
+
try {
|
|
192
|
+
logger_1.nodeLogger.debug(`[${this.name}] 销毁 SSH 主连接`);
|
|
193
|
+
this.sshClient.end();
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
// 忽略销毁错误
|
|
197
|
+
}
|
|
198
|
+
this.sshClient = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
168
201
|
/**
|
|
169
202
|
* 断开连接
|
|
170
203
|
*/
|
|
171
204
|
async disconnect() {
|
|
172
205
|
this.stopMonitoring();
|
|
173
206
|
this.clearTimers();
|
|
207
|
+
this.disposeSshClient();
|
|
174
208
|
this.connector?.dispose();
|
|
175
209
|
this.connector = null;
|
|
176
210
|
this.dockerode = null;
|
|
@@ -286,28 +320,58 @@ class DockerNode {
|
|
|
286
320
|
}
|
|
287
321
|
/**
|
|
288
322
|
* 获取系统信息 (CPU、内存)
|
|
323
|
+
* 优先使用 Docker API,失败时降级到 SSH 命令
|
|
289
324
|
*/
|
|
290
325
|
async getSystemInfo() {
|
|
291
|
-
|
|
326
|
+
// 方式 1: 尝试使用 Docker API
|
|
327
|
+
logger_1.nodeLogger.debug(`[${this.name}] getSystemInfo 调用: dockerode=${!!this.dockerode}, apiAvailable=${this.dockerApiAvailable}`);
|
|
328
|
+
if (this.dockerode && this.dockerApiAvailable) {
|
|
329
|
+
try {
|
|
330
|
+
logger_1.nodeLogger.debug(`[${this.name}] 使用 Docker API 获取系统信息`);
|
|
331
|
+
const info = await this.dockerode.info();
|
|
332
|
+
logger_1.nodeLogger.debug(`[${this.name}] Docker API 返回: NCPU=${info.NCPU}, MemTotal=${info.MemTotal}, MemAvailable=${info.MemAvailable}`);
|
|
333
|
+
const result = {
|
|
334
|
+
NCPU: info.NCPU || 0,
|
|
335
|
+
MemTotal: info.MemTotal || 0,
|
|
336
|
+
MemAvailable: info.MemAvailable, // 可能不存在
|
|
337
|
+
};
|
|
338
|
+
logger_1.nodeLogger.debug(`[${this.name}] 返回系统信息: NCPU=${result.NCPU}, MemTotal=${result.MemTotal}`);
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
logger_1.nodeLogger.warn(`[${this.name}] API getSystemInfo 失败,降级到 SSH: ${e.message}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// 方式 2: SSH 命令行回退
|
|
346
|
+
logger_1.nodeLogger.debug(`[${this.name}] 使用 SSH 命令获取系统信息`);
|
|
347
|
+
if (!this.connector) {
|
|
348
|
+
logger_1.nodeLogger.warn(`[${this.name}] connector 不存在,无法获取系统信息`);
|
|
292
349
|
return null;
|
|
350
|
+
}
|
|
293
351
|
try {
|
|
294
|
-
// 使用
|
|
295
|
-
const result = await this.connector.execWithExitCode('docker info --format "{{
|
|
296
|
-
logger_1.nodeLogger.debug(`[${this.name}] docker info
|
|
297
|
-
// docker info 可能返回退出码 1 但仍有输出(权限问题),只要有输出就解析
|
|
352
|
+
// 使用 JSON 格式获取完整信息,避免字段不存在导致的问题
|
|
353
|
+
const result = await this.connector.execWithExitCode('docker info --format "{{json .}}"');
|
|
354
|
+
logger_1.nodeLogger.debug(`[${this.name}] docker info 输出长度: ${result.output.length}, 退出码: ${result.exitCode}`);
|
|
298
355
|
if (!result.output.trim()) {
|
|
299
356
|
logger_1.nodeLogger.warn(`[${this.name}] docker info 输出为空`);
|
|
300
357
|
return null;
|
|
301
358
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
359
|
+
try {
|
|
360
|
+
const info = JSON.parse(result.output);
|
|
361
|
+
logger_1.nodeLogger.debug(`[${this.name}] SSH docker info 解析: NCPU=${info.NCPU}, MemTotal=${info.MemTotal}, MemAvailable=${info.MemAvailable}`);
|
|
362
|
+
const sshResult = {
|
|
363
|
+
NCPU: info.NCPU || 0,
|
|
364
|
+
MemTotal: info.MemTotal || 0,
|
|
365
|
+
MemAvailable: info.MemAvailable, // 可能不存在
|
|
308
366
|
};
|
|
367
|
+
logger_1.nodeLogger.debug(`[${this.name}] SSH 返回系统信息: NCPU=${sshResult.NCPU}, MemTotal=${sshResult.MemTotal}`);
|
|
368
|
+
return sshResult;
|
|
369
|
+
}
|
|
370
|
+
catch (parseError) {
|
|
371
|
+
logger_1.nodeLogger.warn(`[${this.name}] 解析 docker info JSON 失败: ${parseError}`);
|
|
372
|
+
logger_1.nodeLogger.warn(`[${this.name}] 原始输出: ${result.output.substring(0, 200)}`);
|
|
373
|
+
return null;
|
|
309
374
|
}
|
|
310
|
-
return null;
|
|
311
375
|
}
|
|
312
376
|
catch (e) {
|
|
313
377
|
logger_1.nodeLogger.warn(`[${this.name}] 获取系统信息异常: ${e}`);
|
|
@@ -1178,76 +1242,123 @@ class DockerNode {
|
|
|
1178
1242
|
}
|
|
1179
1243
|
/**
|
|
1180
1244
|
* 初始化 Dockerode
|
|
1181
|
-
*
|
|
1245
|
+
* 建立唯一的 SSH 连接,并通过 `docker system dial-stdio` 复用连接
|
|
1182
1246
|
*/
|
|
1183
|
-
async initDockerode() {
|
|
1247
|
+
async initDockerode(connector) {
|
|
1184
1248
|
try {
|
|
1185
1249
|
let dockerOptions;
|
|
1186
1250
|
// 判断是否是本地节点
|
|
1187
1251
|
const isLocal = this.config.host === '127.0.0.1' || this.config.host === 'localhost';
|
|
1188
1252
|
if (isLocal) {
|
|
1189
|
-
//
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1253
|
+
// 本地连接:直接使用 Unix Socket
|
|
1254
|
+
this.dockerode = new dockerode_1.default({ socketPath: '/var/run/docker.sock' });
|
|
1255
|
+
await this.dockerode.ping();
|
|
1256
|
+
this.dockerApiAvailable = true;
|
|
1257
|
+
logger_1.nodeLogger.info(`[${this.name}] ✅ Docker API 连接成功 (Local Socket)`);
|
|
1258
|
+
return;
|
|
1193
1259
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1260
|
+
// === 远程 SSH 连接配置 (单连接复用方案) ===
|
|
1261
|
+
// 1. 关闭旧连接
|
|
1262
|
+
this.disposeSshClient();
|
|
1263
|
+
// 2. 准备 SSH 配置
|
|
1264
|
+
let portNumber = 22;
|
|
1265
|
+
if (typeof this.config.port === 'number') {
|
|
1266
|
+
portNumber = this.config.port;
|
|
1267
|
+
}
|
|
1268
|
+
else if (typeof this.config.port === 'string') {
|
|
1269
|
+
const parsed = parseInt(this.config.port, 10);
|
|
1270
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
1271
|
+
portNumber = parsed;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const sshConfig = {
|
|
1275
|
+
host: this.config.host,
|
|
1276
|
+
port: portNumber,
|
|
1277
|
+
username: this.credential.username,
|
|
1278
|
+
readyTimeout: 20000,
|
|
1279
|
+
keepaliveInterval: 10000, // 10秒心跳,防止被踢
|
|
1280
|
+
keepaliveCountMax: 3,
|
|
1281
|
+
};
|
|
1282
|
+
// 注入认证信息
|
|
1283
|
+
if (this.credential.authType === 'password' && this.credential.password) {
|
|
1284
|
+
sshConfig.password = this.credential.password;
|
|
1285
|
+
}
|
|
1286
|
+
else if (this.credential.privateKey) {
|
|
1287
|
+
sshConfig.privateKey = this.credential.privateKey.trim();
|
|
1288
|
+
if (this.credential.passphrase) {
|
|
1289
|
+
sshConfig.passphrase = this.credential.passphrase;
|
|
1200
1290
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1291
|
+
}
|
|
1292
|
+
logger_1.nodeLogger.info(`[${this.name}] 正在建立 SSH 主连接...`);
|
|
1293
|
+
// 3. 建立 SSH 连接
|
|
1294
|
+
this.sshClient = new ssh2_1.Client();
|
|
1295
|
+
await new Promise((resolve, reject) => {
|
|
1296
|
+
if (!this.sshClient) {
|
|
1297
|
+
return reject(new Error('SSH client initialization failed'));
|
|
1205
1298
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
port: portNumber,
|
|
1210
|
-
readyTimeout: 20000,
|
|
1299
|
+
const onReady = () => {
|
|
1300
|
+
this.sshClient?.removeListener('error', onError);
|
|
1301
|
+
resolve();
|
|
1211
1302
|
};
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1303
|
+
const onError = (err) => {
|
|
1304
|
+
this.sshClient?.removeListener('ready', onReady);
|
|
1305
|
+
reject(err);
|
|
1306
|
+
};
|
|
1307
|
+
this.sshClient.on('ready', onReady).on('error', onError).connect(sshConfig);
|
|
1308
|
+
});
|
|
1309
|
+
// 监听连接断开,触发重连逻辑
|
|
1310
|
+
this.sshClient.on('close', () => {
|
|
1311
|
+
if (this.status === constants_1.NodeStatus.CONNECTED) {
|
|
1312
|
+
logger_1.nodeLogger.warn(`[${this.name}] SSH 主连接已断开,触发重连`);
|
|
1313
|
+
// 不直接调用 disconnect(),避免状态混乱
|
|
1314
|
+
// 让上层监控逻辑处理重连
|
|
1215
1315
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1316
|
+
});
|
|
1317
|
+
logger_1.nodeLogger.info(`[${this.name}] ✅ SSH 主连接建立成功 (单次登录,复用所有API请求)`);
|
|
1318
|
+
// 4. 创建自定义 Agent,劫持 createConnection
|
|
1319
|
+
// 这允许 dockerode 的所有请求都复用这一个 SSH 连接
|
|
1320
|
+
const agent = new http_1.default.Agent();
|
|
1321
|
+
agent.createConnection = (options, cb) => {
|
|
1322
|
+
logger_1.nodeLogger.debug(`[${this.name}] 🔧 Agent.createConnection 被调用,复用 SSH 隧道`);
|
|
1323
|
+
// 使用 docker system dial-stdio 建立到 Docker Socket 的流
|
|
1324
|
+
// 这是官方 CLI 远程连接的标准方式,支持双向流
|
|
1325
|
+
if (!this.sshClient) {
|
|
1326
|
+
cb(new Error('SSH client not connected'), null);
|
|
1327
|
+
return null;
|
|
1221
1328
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1329
|
+
this.sshClient.exec('docker system dial-stdio', (err, stream) => {
|
|
1330
|
+
if (err) {
|
|
1331
|
+
logger_1.nodeLogger.warn(`[${this.name}] SSH dial-stdio 失败: ${err.message}`);
|
|
1332
|
+
return cb(err, null);
|
|
1333
|
+
}
|
|
1334
|
+
// stream 是双工流,可以直接作为 socket 使用
|
|
1335
|
+
logger_1.nodeLogger.debug(`[${this.name}] ✅ SSH 隧道已建立`);
|
|
1336
|
+
cb(null, stream);
|
|
1337
|
+
});
|
|
1338
|
+
return null;
|
|
1339
|
+
};
|
|
1340
|
+
// 5. 初始化 Dockerode
|
|
1341
|
+
// 使用 'http' 协议欺骗 dockerode 使用我们的 agent
|
|
1342
|
+
dockerOptions = {
|
|
1343
|
+
protocol: 'http',
|
|
1344
|
+
host: '127.0.0.1', // 这里的 host/port 会被 agent 忽略
|
|
1345
|
+
port: 2375,
|
|
1346
|
+
agent: agent,
|
|
1347
|
+
};
|
|
1348
|
+
logger_1.nodeLogger.info(`[${this.name}] 🔨 创建 Dockerode 实例 (使用自定义 Agent)`);
|
|
1235
1349
|
this.dockerode = new dockerode_1.default(dockerOptions);
|
|
1236
|
-
//
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
}
|
|
1242
|
-
catch (e) {
|
|
1243
|
-
this.dockerApiAvailable = false;
|
|
1244
|
-
logger_1.nodeLogger.warn(`[${this.name}] Docker API 连接失败: ${e.message} (将降级使用 SSH 命令)`);
|
|
1245
|
-
}
|
|
1350
|
+
// 测试 API
|
|
1351
|
+
logger_1.nodeLogger.info(`[${this.name}] 🔍 测试 Docker API 连接...`);
|
|
1352
|
+
await this.dockerode.ping();
|
|
1353
|
+
this.dockerApiAvailable = true;
|
|
1354
|
+
logger_1.nodeLogger.info(`[${this.name}] ✅ Docker API 隧道测试成功 (所有请求复用单条 SSH 连接)`);
|
|
1246
1355
|
}
|
|
1247
1356
|
catch (e) {
|
|
1357
|
+
this.disposeSshClient();
|
|
1248
1358
|
this.dockerode = null;
|
|
1249
1359
|
this.dockerApiAvailable = false;
|
|
1250
|
-
logger_1.nodeLogger.
|
|
1360
|
+
logger_1.nodeLogger.warn(`[${this.name}] Docker API 隧道建立失败: ${e.message}`);
|
|
1361
|
+
throw e; // 抛出错误让 connect 方法处理降级
|
|
1251
1362
|
}
|
|
1252
1363
|
}
|
|
1253
1364
|
/**
|
|
@@ -1638,15 +1749,21 @@ class DockerNode {
|
|
|
1638
1749
|
}
|
|
1639
1750
|
/**
|
|
1640
1751
|
* 启动 API 健康检查
|
|
1752
|
+
* DPanel模式:信任底层 Keep-Alive,不主动 Ping,只在操作报错时重连
|
|
1641
1753
|
*/
|
|
1642
1754
|
startHealthCheck() {
|
|
1643
|
-
//
|
|
1755
|
+
// 方案:移除定时器,改为惰性检查
|
|
1756
|
+
// 底层 keepaliveInterval: 15s 的静默心跳已经足够防止断连
|
|
1757
|
+
// 主动 Ping 是产生日志的元凶,必须移除
|
|
1758
|
+
// 仅在启动时检查一次,确保 API 正常
|
|
1644
1759
|
this.checkApiHealth();
|
|
1645
|
-
//
|
|
1760
|
+
// 不再设置定时器,完全信任底层 TCP Keep-Alive
|
|
1761
|
+
/*
|
|
1646
1762
|
this.healthCheckTimer = setInterval(async () => {
|
|
1647
|
-
|
|
1648
|
-
},
|
|
1649
|
-
|
|
1763
|
+
await this.checkApiHealth()
|
|
1764
|
+
}, CHECK_INTERVAL)
|
|
1765
|
+
*/
|
|
1766
|
+
logger_1.nodeLogger.debug(`[${this.name}] API健康检查策略: 仅启动时检查 (依赖底层 TCP Keep-Alive 保活,无定时Ping)`);
|
|
1650
1767
|
}
|
|
1651
1768
|
/**
|
|
1652
1769
|
* 检查 Docker API 健康状态
|
|
@@ -1674,7 +1791,8 @@ class DockerNode {
|
|
|
1674
1791
|
// API健康,无需操作
|
|
1675
1792
|
}
|
|
1676
1793
|
catch (e) {
|
|
1677
|
-
logger_1.nodeLogger.
|
|
1794
|
+
logger_1.nodeLogger.error(`[${this.name}] ❌ Docker API 健康检查失败: ${e.message}`);
|
|
1795
|
+
logger_1.nodeLogger.warn(`[${this.name}] ⚠ API失败后将进入降级模式,每${constants_1.DEGRADED_POLL_INTERVAL / 1000}秒执行一次SSH命令`);
|
|
1678
1796
|
this.dockerApiAvailable = false;
|
|
1679
1797
|
this.startDegradedPolling();
|
|
1680
1798
|
}
|
|
@@ -1700,7 +1818,8 @@ class DockerNode {
|
|
|
1700
1818
|
this.degradedPollTimer = setInterval(async () => {
|
|
1701
1819
|
await this.pollContainerStates();
|
|
1702
1820
|
}, constants_1.DEGRADED_POLL_INTERVAL);
|
|
1703
|
-
logger_1.nodeLogger.
|
|
1821
|
+
logger_1.nodeLogger.warn(`[${this.name}] ⚠ 进入降级模式: 每${constants_1.DEGRADED_POLL_INTERVAL / 1000}秒执行一次SSH命令查询容器状态`);
|
|
1822
|
+
logger_1.nodeLogger.warn(`[${this.name}] ⚠ 这是产生频繁SSH登录记录的主要原因!建议修复Docker API连接以减少SSH使用`);
|
|
1704
1823
|
}
|
|
1705
1824
|
/**
|
|
1706
1825
|
* 停止降级轮询
|
|
@@ -1714,7 +1833,7 @@ class DockerNode {
|
|
|
1714
1833
|
clearInterval(this.degradedPollTimer);
|
|
1715
1834
|
this.degradedPollTimer = null;
|
|
1716
1835
|
}
|
|
1717
|
-
logger_1.nodeLogger.info(`[${this.name}]
|
|
1836
|
+
logger_1.nodeLogger.info(`[${this.name}] ✅ Docker API已恢复,停止降级轮询 (不再频繁执行SSH命令)`);
|
|
1718
1837
|
}
|
|
1719
1838
|
/**
|
|
1720
1839
|
* 轮询容器状态 (用于降级模式)
|
|
@@ -1723,9 +1842,10 @@ class DockerNode {
|
|
|
1723
1842
|
if (this.status !== constants_1.NodeStatus.CONNECTED)
|
|
1724
1843
|
return;
|
|
1725
1844
|
try {
|
|
1845
|
+
logger_1.nodeLogger.debug(`[${this.name}] 🔍 执行降级轮询: 使用SSH命令查询容器状态 (这会产生SSH登录记录)`);
|
|
1726
1846
|
const containers = await this.listContainers(true);
|
|
1727
1847
|
this.checkContainerStateChanges(containers);
|
|
1728
|
-
logger_1.nodeLogger.debug(`[${this.name}]
|
|
1848
|
+
logger_1.nodeLogger.debug(`[${this.name}] 降级轮询完成: 检查了 ${containers.length} 个容器`);
|
|
1729
1849
|
}
|
|
1730
1850
|
catch (e) {
|
|
1731
1851
|
logger_1.nodeLogger.warn(`[${this.name}] 降级轮询失败: ${e}`);
|
|
@@ -1733,42 +1853,127 @@ class DockerNode {
|
|
|
1733
1853
|
}
|
|
1734
1854
|
/**
|
|
1735
1855
|
* 启动 Docker 事件流监听
|
|
1856
|
+
* 优先使用 Docker API (长连接且有心跳),失败降级到 SSH 命令
|
|
1736
1857
|
*/
|
|
1737
|
-
startEventStream() {
|
|
1738
|
-
|
|
1739
|
-
return;
|
|
1740
|
-
// 防止并发启动:使用 _startingStream 标志
|
|
1858
|
+
async startEventStream() {
|
|
1859
|
+
// 防止并发启动
|
|
1741
1860
|
if (this._startingStream) {
|
|
1742
1861
|
logger_1.nodeLogger.debug(`[${this.name}] 事件流正在启动中,跳过`);
|
|
1743
1862
|
return;
|
|
1744
1863
|
}
|
|
1745
1864
|
;
|
|
1746
1865
|
this._startingStream = true;
|
|
1747
|
-
//
|
|
1748
|
-
if (this.
|
|
1749
|
-
|
|
1866
|
+
// 清理旧的流
|
|
1867
|
+
if (this._eventStreamStop) {
|
|
1868
|
+
try {
|
|
1869
|
+
this._eventStreamStop();
|
|
1870
|
+
this._eventStreamStop = null;
|
|
1871
|
+
}
|
|
1872
|
+
catch (e) {
|
|
1873
|
+
// 忽略清理错误
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
logger_1.nodeLogger.info(`[${this.name}] 🚀 启动事件流监听...`);
|
|
1877
|
+
// === 方案 1: 优先使用 Docker API (dockerode) ===
|
|
1878
|
+
// 优点: 复用已有的 Keep-Alive 连接,不会因为静默被防火墙切断
|
|
1879
|
+
if (this.dockerode && this.dockerApiAvailable) {
|
|
1880
|
+
try {
|
|
1881
|
+
logger_1.nodeLogger.info(`[${this.name}] 尝试使用 Docker API 获取事件流 (推荐模式,有心跳保护)`);
|
|
1882
|
+
logger_1.nodeLogger.info(`[${this.name}] 🔍 调用 dockerode.getEvents() (这可能会建立新的 SSH 连接)`);
|
|
1883
|
+
const stream = await this.dockerode.getEvents({
|
|
1884
|
+
filters: { type: ['container'] }
|
|
1885
|
+
});
|
|
1886
|
+
logger_1.nodeLogger.info(`[${this.name}] ✅ getEvents() 成功返回流对象`);
|
|
1887
|
+
// 处理数据流
|
|
1888
|
+
stream.on('data', (chunk) => {
|
|
1889
|
+
try {
|
|
1890
|
+
const lines = chunk.toString().split('\n').filter(Boolean);
|
|
1891
|
+
for (const line of lines) {
|
|
1892
|
+
this.handleEventLine(line);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
catch (e) {
|
|
1896
|
+
logger_1.nodeLogger.debug(`[${this.name}] 处理事件数据失败: ${e}`);
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
// 处理错误和断开
|
|
1900
|
+
const onStreamError = (err) => {
|
|
1901
|
+
if (this._startingStream === false)
|
|
1902
|
+
return; // 已经手动停止
|
|
1903
|
+
logger_1.nodeLogger.warn(`[${this.name}] API 事件流异常: ${err.message || 'Stream ended'}`);
|
|
1904
|
+
this.restartEventStream();
|
|
1905
|
+
};
|
|
1906
|
+
stream.on('error', onStreamError);
|
|
1907
|
+
stream.on('end', () => onStreamError(new Error('Stream ended')));
|
|
1908
|
+
stream.on('close', () => onStreamError(new Error('Stream closed')));
|
|
1909
|
+
this._eventStreamStop = () => {
|
|
1910
|
+
try {
|
|
1911
|
+
stream.destroy?.();
|
|
1912
|
+
stream.off('error', onStreamError);
|
|
1913
|
+
stream.off('end', onStreamError);
|
|
1914
|
+
stream.off('close', onStreamError);
|
|
1915
|
+
stream.off('data', () => { });
|
|
1916
|
+
}
|
|
1917
|
+
catch (e) {
|
|
1918
|
+
// 忽略清理错误
|
|
1919
|
+
}
|
|
1920
|
+
};
|
|
1921
|
+
this._startingStream = false;
|
|
1922
|
+
logger_1.nodeLogger.info(`[${this.name}] ✅ API 事件流已连接 (享受心跳保护,不会超时)`);
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
catch (e) {
|
|
1926
|
+
logger_1.nodeLogger.warn(`[${this.name}] API 事件流启动失败: ${e.message},降级到 SSH 命令`);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
// === 方案 2: 降级使用 SSH 命令行 ===
|
|
1930
|
+
// 只有 API 不可用时才走这里(可能因静默超时而频繁重连)
|
|
1931
|
+
if (!this.connector) {
|
|
1932
|
+
;
|
|
1750
1933
|
this._startingStream = false;
|
|
1934
|
+
logger_1.nodeLogger.warn(`[${this.name}] 无可用连接器,跳过事件流监听`);
|
|
1751
1935
|
return;
|
|
1752
1936
|
}
|
|
1753
|
-
;
|
|
1754
|
-
this._activeStreamCount = this._activeStreamCount || 0;
|
|
1755
|
-
this._activeStreamCount++;
|
|
1756
|
-
logger_1.nodeLogger.debug(`[${this.name}] 启动事件流 (活跃数: ${this._activeStreamCount})`);
|
|
1937
|
+
logger_1.nodeLogger.warn(`[${this.name}] 使用 SSH 命令模式监听事件流 (注意: 可能因长时间静默被防火墙切断)`);
|
|
1757
1938
|
this.connector.startEventStream((line) => {
|
|
1758
1939
|
this.handleEventLine(line);
|
|
1759
1940
|
}).then((stop) => {
|
|
1760
1941
|
;
|
|
1761
1942
|
this._eventStreamStop = stop;
|
|
1762
1943
|
this._startingStream = false;
|
|
1763
|
-
logger_1.nodeLogger.
|
|
1944
|
+
logger_1.nodeLogger.info(`[${this.name}] ✅ SSH 事件流已连接 (注意: SSH模式下可能因静默超时而频繁重连)`);
|
|
1764
1945
|
}).catch((err) => {
|
|
1765
1946
|
;
|
|
1766
|
-
this._activeStreamCount--;
|
|
1767
1947
|
this._startingStream = false;
|
|
1768
|
-
logger_1.nodeLogger.
|
|
1769
|
-
|
|
1948
|
+
logger_1.nodeLogger.error(`[${this.name}] ❌ SSH 事件流启动失败: ${err.message}`);
|
|
1949
|
+
this.restartEventStream();
|
|
1770
1950
|
});
|
|
1771
1951
|
}
|
|
1952
|
+
/**
|
|
1953
|
+
* 重启事件流
|
|
1954
|
+
*/
|
|
1955
|
+
restartEventStream() {
|
|
1956
|
+
// 清理旧的流
|
|
1957
|
+
if (this._eventStreamStop) {
|
|
1958
|
+
try {
|
|
1959
|
+
this._eventStreamStop();
|
|
1960
|
+
this._eventStreamStop = null;
|
|
1961
|
+
}
|
|
1962
|
+
catch (e) {
|
|
1963
|
+
// 忽略清理错误
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
// 重置启动标志
|
|
1967
|
+
;
|
|
1968
|
+
this._startingStream = false;
|
|
1969
|
+
// 5秒后重试
|
|
1970
|
+
setTimeout(() => {
|
|
1971
|
+
if (this.status === constants_1.NodeStatus.CONNECTED) {
|
|
1972
|
+
logger_1.nodeLogger.info(`[${this.name}] 重新启动事件流...`);
|
|
1973
|
+
this.startEventStream();
|
|
1974
|
+
}
|
|
1975
|
+
}, 5000);
|
|
1976
|
+
}
|
|
1772
1977
|
/**
|
|
1773
1978
|
* 处理事件流中的一行数据
|
|
1774
1979
|
*/
|