zen-gitsync 2.16.33 → 2.16.35
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/package.json +1 -1
- package/src/ui/public/assets/AgentView-BsN9y4Kg.css +1 -0
- package/src/ui/public/assets/AgentView-Bx7-GoVN.js +4 -0
- package/src/ui/public/assets/AppVersionBadge-CSPVpIfv.js +2 -0
- package/src/ui/public/assets/AppVersionBadge-Dj6H729N.css +1 -0
- package/src/ui/public/assets/BranchSelector-T_rJm-JY.css +1 -0
- package/src/ui/public/assets/BranchSelector-Y8ZDgSSC.js +1 -0
- package/src/ui/public/assets/CommitForm-Bfm3h-xP.js +6 -0
- package/src/ui/public/assets/CommitForm-CUvl0VcF.css +1 -0
- package/src/ui/public/assets/CommonDialog-Cyh3xu0p.css +1 -0
- package/src/ui/public/assets/CommonDialog-qme3em9i.js +1 -0
- package/src/ui/public/assets/EditorView-013TBMVC.js +1 -0
- package/src/ui/public/assets/EditorView-BgA-_8WV.css +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-B5kh7VxS.css +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-DKnli1z2.js +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BAQDU9Jd.css +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BZH01Rh6.js +21 -0
- package/src/ui/public/assets/LogList-5Br9Wb_B.js +7 -0
- package/src/ui/public/assets/LogList-r8sgFBhZ.css +1 -0
- package/src/ui/public/assets/MindmapView-77qraT9V.js +1 -0
- package/src/ui/public/assets/MindmapView-CS2t30Ko.css +1 -0
- package/src/ui/public/assets/MonitorView-BS65Jur8.js +1 -0
- package/src/ui/public/assets/MonitorView-DxFogQIt.css +1 -0
- package/src/ui/public/assets/ProjectStartupButton-B7zvqRj_.css +1 -0
- package/src/ui/public/assets/ProjectStartupButton-I69v-Nsd.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-BZuK45Ct.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-d0klonVb.css +1 -0
- package/src/ui/public/assets/SourceMapView-9F8fHJtK.js +3 -0
- package/src/ui/public/assets/SourceMapView-DG_a_076.css +1 -0
- package/src/ui/public/assets/{SvgIcon-CDNsk4-Q.css → SvgIcon-BQ6bR7l0.css} +1 -1
- package/src/ui/public/assets/SvgIcon-C6rY7Uo8.js +1 -0
- package/src/ui/public/assets/UserInputNode-2igLF_7v.js +1 -0
- package/src/ui/public/assets/UserInputNode-DDtvv9Iq.css +1 -0
- package/src/ui/public/assets/WorkbenchView-Bnz_19Gn.js +12 -0
- package/src/ui/public/assets/{WorkbenchView-Bp2YaSGS.css → WorkbenchView-CcYljxjT.css} +1 -1
- package/src/ui/public/assets/{_plugin-vue_export-helper-DkdPQgM8.js → _plugin-vue_export-helper-DidySQ8u.js} +1 -1
- package/src/ui/public/assets/{configStore-C2xoYf_U.js → configStore-CRJLI_GA.js} +1 -1
- package/src/ui/public/assets/element-plus-gSoHVQIp.js +27 -0
- package/src/ui/public/assets/{flow-mindmap-uF7lcnUF.js → flow-mindmap-DDBt2Quw.js} +13 -13
- package/src/ui/public/assets/{index-DkMUEB4_.css → index-BNYdpyC2.css} +1 -1
- package/src/ui/public/assets/index-DG6MOQX-.js +46 -0
- package/src/ui/public/assets/office-docx-CHpl_i5B.js +31 -0
- package/src/ui/public/assets/{office-excel-BhBLQBXN.js → office-excel-Dl4rqXiC.js} +1 -1
- package/src/ui/public/assets/office-pptx-DPjuekp6.js +60 -0
- package/src/ui/public/assets/socket-io-z4n873W1.js +1 -0
- package/src/ui/public/assets/{toolsStore-Dpb3rc-o.js → toolsStore-BlWdtCS3.js} +1 -1
- package/src/ui/public/assets/{vendor-BuJWEuf4.js → vendor-CRyxl6wl.js} +39 -46
- package/src/ui/public/assets/vue-flow-DTNU5FFo.js +39 -0
- package/src/ui/public/index.html +14 -14
- package/src/ui/server/utils/startServerOnAvailablePort.js +265 -134
- package/src/ui/server/utils/startServerOnAvailablePort.test.js +184 -84
- package/src/ui/public/assets/AgentView-8-xsOmE6.css +0 -1
- package/src/ui/public/assets/AgentView-D9y5maA3.js +0 -4
- package/src/ui/public/assets/AppVersionBadge-CS7Trvp-.css +0 -1
- package/src/ui/public/assets/AppVersionBadge-CsKq0HLG.js +0 -2
- package/src/ui/public/assets/BranchSelector-B0eLkAdv.css +0 -1
- package/src/ui/public/assets/BranchSelector-C09wdMBR.js +0 -1
- package/src/ui/public/assets/CommitForm-DdK8dymc.css +0 -1
- package/src/ui/public/assets/CommitForm-wTVAlj8K.js +0 -6
- package/src/ui/public/assets/CommonDialog-ChSKsEz2.css +0 -1
- package/src/ui/public/assets/CommonDialog-gF_3v5QY.js +0 -1
- package/src/ui/public/assets/EditorView-BYaXYY5H.js +0 -1
- package/src/ui/public/assets/EditorView-DZMLvQds.css +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-DvSe3GqC.js +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-jNHI5zdm.css +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CYGp-PqS.css +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-FYLZxzMC.js +0 -21
- package/src/ui/public/assets/LogList-BB56Ma_-.js +0 -7
- package/src/ui/public/assets/LogList-BlJplIi1.css +0 -1
- package/src/ui/public/assets/MindmapView-B57NJ-tK.js +0 -2
- package/src/ui/public/assets/MindmapView-BQTqx842.css +0 -1
- package/src/ui/public/assets/MonitorView-DujOLv6G.js +0 -1
- package/src/ui/public/assets/MonitorView-TaS4kL0Y.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-B0vIxM8Y.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-QLPltCK5.css +0 -1
- package/src/ui/public/assets/RemoteRepoCard-BRGNlbeO.css +0 -1
- package/src/ui/public/assets/RemoteRepoCard-Tdhzf-Jb.js +0 -5
- package/src/ui/public/assets/SourceMapView-BNRjvgzH.css +0 -1
- package/src/ui/public/assets/SourceMapView-Bf7__bEW.js +0 -3
- package/src/ui/public/assets/SvgIcon-DV0F5fm7.js +0 -1
- package/src/ui/public/assets/UserInputNode-BVgQkB2l.css +0 -1
- package/src/ui/public/assets/UserInputNode-ZatmWGxI.js +0 -1
- package/src/ui/public/assets/WorkbenchView-DVjvpQMi.js +0 -28
- package/src/ui/public/assets/element-plus-BrZIKMTV.js +0 -47
- package/src/ui/public/assets/index-DDa4fUq_.js +0 -79
- package/src/ui/public/assets/office-docx-DZfJaVF6.js +0 -32
- package/src/ui/public/assets/office-pptx-C0yb5pVU.js +0 -74
- package/src/ui/public/assets/socket-io-Bcae2P7B.js +0 -1
- package/src/ui/public/assets/vue-flow-CrEicpfB.js +0 -39
|
@@ -1,134 +1,265 @@
|
|
|
1
|
-
// Copyright 2026 xz333221
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
//
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
import { execFileSync } from 'node:child_process'
|
|
16
|
+
import logger from './logger.js'
|
|
17
|
+
|
|
18
|
+
// Windows 会把成块的端口划进"排除范围"(Hyper-V / Docker / WSL 预留,或系统保留的
|
|
19
|
+
// 动态端口块)。落在这些段里的端口,listen() 报的是 EACCES 而不是 EADDRINUSE ——
|
|
20
|
+
// 端口明明没人占,系统就是不让绑。查看方式:
|
|
21
|
+
// netsh int ip show excludedportrange protocol=tcp
|
|
22
|
+
// 这些段通常连续上百个端口,靠 +1 逐个撞过去极慢(旧实现每个端口还要 sleep 800ms,
|
|
23
|
+
// 撞进一段 100 端口的保留区就是 80 秒)。所以这里懒加载一次并缓存:
|
|
24
|
+
// 只有真撞到 EACCES 时才去查 netsh,正常启动路径零开销。
|
|
25
|
+
let excludedRanges = null
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 解析 `netsh int ip show excludedportrange protocol=tcp` 的输出。
|
|
29
|
+
* 纯函数,单独导出以便测试(表头随系统语言变化,解析规则必须稳)。
|
|
30
|
+
* @param {string} output netsh 原始输出
|
|
31
|
+
* @returns {Array<[number, number]>} 保留端口段 [起始, 结束] 列表
|
|
32
|
+
*/
|
|
33
|
+
export function parseExcludedPortRanges(output) {
|
|
34
|
+
const ranges = []
|
|
35
|
+
if (typeof output !== 'string' || !output) return ranges
|
|
36
|
+
|
|
37
|
+
// 表头随系统语言变化(开始端口/结束端口 vs Start Port/End Port),
|
|
38
|
+
// 只认数据行:行尾跟着两个十进制数,行尾的 * 表示"管理的端口排除"
|
|
39
|
+
const re = /(\d{1,5})\s+(\d{1,5})\s*\**\s*$/gm
|
|
40
|
+
let match
|
|
41
|
+
while ((match = re.exec(output)) !== null) {
|
|
42
|
+
const start = Number(match[1])
|
|
43
|
+
const end = Number(match[2])
|
|
44
|
+
if (Number.isInteger(start) && Number.isInteger(end) && start > 0 && end >= start && end < 65536) {
|
|
45
|
+
ranges.push([start, end])
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return ranges
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getExcludedPortRanges() {
|
|
52
|
+
if (excludedRanges !== null) return excludedRanges
|
|
53
|
+
excludedRanges = []
|
|
54
|
+
|
|
55
|
+
if (process.platform !== 'win32') return excludedRanges
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const output = execFileSync(
|
|
59
|
+
'netsh',
|
|
60
|
+
['int', 'ip', 'show', 'excludedportrange', 'protocol=tcp'],
|
|
61
|
+
{ encoding: 'utf8', timeout: 3000, windowsHide: true }
|
|
62
|
+
)
|
|
63
|
+
excludedRanges = parseExcludedPortRanges(output)
|
|
64
|
+
if (excludedRanges.length) {
|
|
65
|
+
logger.info(`检测到 ${excludedRanges.length} 个系统保留端口段,启动时将自动跳过`)
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// 查不到(非 Windows / netsh 不可用 / 超时)就退化成逐个 +1 试,不影响主流程
|
|
69
|
+
logger.info(`查询系统保留端口段失败,退化为逐端口探测: ${err?.message || err}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return excludedRanges
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 端口是否落在系统保留段内,是则返回该段,否则返回 null。
|
|
77
|
+
* 注意:内部会触发 netsh 查询,只在确认需要时调用。
|
|
78
|
+
*/
|
|
79
|
+
function findExcludedRange(port) {
|
|
80
|
+
return getExcludedPortRanges().find(([start, end]) => port >= start && port <= end) || null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 监听地址:默认只绑回环地址。
|
|
84
|
+
// GUI 服务没有任何认证层,绑 0.0.0.0 等于把 /api/exec-stream(命令执行)、
|
|
85
|
+
// /api/add-npm-script(写 package.json scripts)、/api/open-file(用系统
|
|
86
|
+
// 关联程序打开任意文件)等接口裸奔在局域网上,同网段任何主机都可调用。
|
|
87
|
+
// 确有远程访问 / WSL 跨环境需求时,用 ZEN_HOST=0.0.0.0 显式放开。
|
|
88
|
+
const DEFAULT_HOST = '127.0.0.1'
|
|
89
|
+
|
|
90
|
+
// 浏览器访问地址不要使用 localhost:Windows 上它可能优先解析到 ::1,
|
|
91
|
+
// 而另一个程序恰好占用 IPv6 同端口时,页面会被打开到错误的服务。
|
|
92
|
+
// 默认服务明确绑定 IPv4 回环,因此浏览器也必须使用同一个地址。
|
|
93
|
+
function getBrowserHost(bindHost) {
|
|
94
|
+
return bindHost === DEFAULT_HOST ? DEFAULT_HOST : 'localhost'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeHost(raw) {
|
|
98
|
+
const value = typeof raw === 'string' ? raw.trim() : ''
|
|
99
|
+
return value || DEFAULT_HOST
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function startServerOnAvailablePort({
|
|
103
|
+
httpServer,
|
|
104
|
+
startPort,
|
|
105
|
+
chalk,
|
|
106
|
+
open,
|
|
107
|
+
noOpen,
|
|
108
|
+
isGitRepo,
|
|
109
|
+
savePortToFile,
|
|
110
|
+
fsSync,
|
|
111
|
+
maxTries = 100,
|
|
112
|
+
callbackExecutedRef,
|
|
113
|
+
host
|
|
114
|
+
}) {
|
|
115
|
+
const bindHost = normalizeHost(host ?? process.env.ZEN_HOST)
|
|
116
|
+
let currentPort = startPort;
|
|
117
|
+
const maxPort = startPort + maxTries;
|
|
118
|
+
const getCallbackExecuted = () => {
|
|
119
|
+
if (callbackExecutedRef && typeof callbackExecutedRef === 'object' && 'value' in callbackExecutedRef) {
|
|
120
|
+
return Boolean(callbackExecutedRef.value);
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const setCallbackExecuted = (value) => {
|
|
126
|
+
if (callbackExecutedRef && typeof callbackExecutedRef === 'object' && 'value' in callbackExecutedRef) {
|
|
127
|
+
callbackExecutedRef.value = Boolean(value);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
let lastErrorCode = null;
|
|
132
|
+
|
|
133
|
+
while (currentPort < maxPort) {
|
|
134
|
+
try {
|
|
135
|
+
if (currentPort > startPort) {
|
|
136
|
+
// 系统保留端口(EACCES)不会随时间释放,没必要等;
|
|
137
|
+
// 只有"被占用"(EADDRINUSE)才需要给端口一点时间释放
|
|
138
|
+
if (lastErrorCode !== 'EACCES') {
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
140
|
+
}
|
|
141
|
+
logger.info(`尝试端口 ${currentPort}...`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 本次尝试要绑的端口。回调里必须用它校验,不能用外层的 currentPort:
|
|
145
|
+
// 闭包捕获的是同一个变量,重试时它的值早就变了
|
|
146
|
+
const attemptPort = currentPort;
|
|
147
|
+
|
|
148
|
+
await new Promise((resolve, reject) => {
|
|
149
|
+
// 每次尝试独立的 settled。失败之后即便平台补发了 listening 回调,也不再响应
|
|
150
|
+
let settled = false;
|
|
151
|
+
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
httpServer.removeListener('error', errorHandler);
|
|
154
|
+
httpServer.removeListener('listening', listeningHandler);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const errorHandler = (err) => {
|
|
158
|
+
if (settled) return;
|
|
159
|
+
settled = true;
|
|
160
|
+
cleanup();
|
|
161
|
+
reject(err);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const listeningHandler = () => {
|
|
165
|
+
// 过期回调防护。server.listen(port, host, cb) 的 cb 会被注册成
|
|
166
|
+
// once('listening', cb),而 listen 失败时只 emit 'error',这个 cb
|
|
167
|
+
// 摘不掉。等后面某次 listen 成功 emit 'listening',历史残留的 cb
|
|
168
|
+
// 会和新 cb 一起被依次触发 —— 残留 cb 抢先执行会把防重入标志置位,
|
|
169
|
+
// 本次真正成功的回调就会直接 return,await 永久挂起,服务起不来。
|
|
170
|
+
// 这里用"本次是否已 settle"+"实际监听端口是否为本次目标"双重拦截。
|
|
171
|
+
if (settled) return;
|
|
172
|
+
const addr = httpServer.address();
|
|
173
|
+
if (addr && addr.port !== attemptPort) return;
|
|
174
|
+
|
|
175
|
+
settled = true;
|
|
176
|
+
cleanup();
|
|
177
|
+
|
|
178
|
+
if (getCallbackExecuted()) return;
|
|
179
|
+
setCallbackExecuted(true);
|
|
180
|
+
|
|
181
|
+
console.log(chalk.green('======================================'));
|
|
182
|
+
console.log(chalk.green(` Zen GitSync 服务器已启动`));
|
|
183
|
+
const browserHost = getBrowserHost(bindHost);
|
|
184
|
+
console.log(chalk.green(` 访问地址: http://${browserHost}:${attemptPort}`));
|
|
185
|
+
console.log(chalk.green(` 启动时间: ${new Date().toLocaleString()}`));
|
|
186
|
+
// 非回环地址才提示,默认情况保持安静(避免每次启动都刷警告)
|
|
187
|
+
if (bindHost !== DEFAULT_HOST) {
|
|
188
|
+
console.log(chalk.yellow(` 监听地址: ${bindHost}(已放开非本地访问,请确认网络环境可信)`));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (isGitRepo) {
|
|
192
|
+
console.log(chalk.green(` 当前目录是Git仓库`));
|
|
193
|
+
} else {
|
|
194
|
+
console.log(chalk.yellow(` 当前目录不是Git仓库,文件监控未启动`));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(chalk.green('======================================'));
|
|
198
|
+
|
|
199
|
+
savePortToFile(attemptPort);
|
|
200
|
+
|
|
201
|
+
// 自拉起重启通知:父进程 spawn 时设置了 ZEN_RESTART_NOTIFY_PATH
|
|
202
|
+
// 子进程成功 bind 端口后,把端口写到该文件作为"已就绪"信号
|
|
203
|
+
// (instanceRegistry 在 EPERM 环境下不可用,这是回退通道)
|
|
204
|
+
const notifyPath = process.env.ZEN_RESTART_NOTIFY_PATH;
|
|
205
|
+
if (notifyPath && fsSync) {
|
|
206
|
+
try {
|
|
207
|
+
fsSync.writeFileSync(notifyPath, String(attemptPort), 'utf8');
|
|
208
|
+
} catch (_) {
|
|
209
|
+
// 通知写失败不影响主流程
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!noOpen) {
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
open(`http://${browserHost}:${attemptPort}`);
|
|
216
|
+
}, 0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
resolve();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// 不把成功回调挂在 listen() 的第三个参数上:那样它会被注册成
|
|
223
|
+
// once('listening', cb),而 listen 失败时只 emit 'error',这个 cb
|
|
224
|
+
// 摘不掉。显式注册后,失败路径的 cleanup() 能把它清理干净。
|
|
225
|
+
httpServer.once('error', errorHandler);
|
|
226
|
+
httpServer.once('listening', listeningHandler);
|
|
227
|
+
httpServer.listen(attemptPort, bindHost);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return currentPort;
|
|
231
|
+
} catch (err) {
|
|
232
|
+
lastErrorCode = err?.code || null;
|
|
233
|
+
|
|
234
|
+
if (err.code === 'EADDRINUSE') {
|
|
235
|
+
logger.info(`端口 ${currentPort} 被占用,尝试下一个端口...`);
|
|
236
|
+
currentPort++;
|
|
237
|
+
} else if (err.code === 'EACCES') {
|
|
238
|
+
// 端口不让绑:Windows 上绝大多数情况落在系统保留段里。
|
|
239
|
+
// 命中整段就直接跳到段尾之后,否则逐个撞过去会非常慢。
|
|
240
|
+
const range = findExcludedRange(currentPort);
|
|
241
|
+
if (range) {
|
|
242
|
+
logger.info(`端口 ${currentPort} 位于系统保留段 ${range[0]}-${range[1]},跳到 ${range[1] + 1}...`);
|
|
243
|
+
currentPort = range[1] + 1;
|
|
244
|
+
} else {
|
|
245
|
+
logger.info(`端口 ${currentPort} 无监听权限(可能被系统保留),尝试下一个端口...`);
|
|
246
|
+
currentPort++;
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
logger.error('启动服务器失败:', err);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
logger.error(`无法找到可用端口 (尝试范围: ${startPort}-${maxPort - 1})`);
|
|
256
|
+
if (process.platform === 'win32' && getExcludedPortRanges().length) {
|
|
257
|
+
logger.error(
|
|
258
|
+
`已跳过的系统保留端口段: ${getExcludedPortRanges()
|
|
259
|
+
.map(([s, e]) => `${s}-${e}`)
|
|
260
|
+
.join(', ')}`
|
|
261
|
+
);
|
|
262
|
+
logger.error('可用 netsh int ip show excludedportrange protocol=tcp 复核;也可用 PORT 环境变量指定其它端口');
|
|
263
|
+
}
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|