Skip to content

Commit 0568d59

Browse files
committed
chore: add probe
1 parent 4ee0141 commit 0568d59

File tree

10 files changed

+411
-46
lines changed

10 files changed

+411
-46
lines changed

cmd/runner-manager/templates/index.html

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,28 @@
117117
.modal-body .row { margin-bottom: 12px; }
118118
.modal-body .row label { display: block; color: var(--muted); font-size: 12px; margin-bottom: 4px; }
119119
.modal-body .row .val { font-family: ui-monospace, monospace; font-size: 13px; word-break: break-all; }
120+
.btn-reveal-fix {
121+
padding: 4px 10px;
122+
font-size: 12px;
123+
background: transparent;
124+
color: var(--warn);
125+
border: 1px solid var(--warn);
126+
border-radius: 4px;
127+
cursor: pointer;
128+
margin-right: 8px;
129+
}
130+
.btn-reveal-fix:hover { opacity: 0.9; }
131+
.btn-copy-cmd {
132+
padding: 4px 10px;
133+
font-size: 12px;
134+
background: transparent;
135+
color: var(--accent);
136+
border: 1px solid var(--accent);
137+
border-radius: 4px;
138+
cursor: pointer;
139+
margin-right: 8px;
140+
}
141+
.btn-copy-cmd:hover { opacity: 0.9; }
120142
.modal-body input, .modal-body select { width: 100%; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--text); margin-top: 4px; }
121143
.modal-footer { padding: 12px 20px; border-top: 1px solid var(--border); }
122144
.modal-footer button { margin-right: 8px; }
@@ -197,7 +219,7 @@ <h2>Runner 列表</h2>
197219
<td>
198220
<span class="badge {{.Status}}">{{.Status}}</span>
199221
{{if .Running}}<span class="badge running">运行中</span>{{end}}
200-
{{if .ProbeError}}<br><span class="probe-err" title="{{.ProbeError}}">探测失败</span>{{end}}
222+
{{if .Probe}}<br><span class="probe-err" title="{{.Probe.Error}}">探测失败{{if .Probe.Type}} ({{.Probe.Type}}){{end}}</span>{{end}}
201223
</td>
202224
{{if $.Config.Runners.ContainerMode}}<td><code>{{.JobDockerBackend}}</code></td>{{end}}
203225
<td>
@@ -222,6 +244,10 @@ <h2>Runner 列表</h2>
222244
<td>
223245
{{if and (eq .Status "installed") (not .Running)}}<button type="button" class="btn-start" data-name="{{.Name}}" title="启动 Runner">启动</button>{{end}}
224246
{{if .Running}}<button type="button" class="btn-stop" data-name="{{.Name}}" title="停止 Runner">停止</button>{{end}}
247+
{{if eq .Status "unknown"}}
248+
<button type="button" class="btn-start" data-name="{{.Name}}" title="状态探测失败,尝试启动 Runner">启动</button>
249+
<button type="button" class="btn-stop" data-name="{{.Name}}" title="状态探测失败,尝试停止 Runner">停止</button>
250+
{{end}}
225251
<button type="button" class="btn-view" data-name="{{.Name}}" title="查看配置">查看</button>
226252
<button type="button" class="btn-edit" data-name="{{.Name}}" title="编辑配置">编辑</button>
227253
<button type="button" class="btn-del" data-name="{{.Name}}" title="从配置中移除">删除</button>
@@ -285,6 +311,10 @@ <h3 id="modalTitle">Runner 配置</h3>
285311
<div class="row"><label>安装目录</label><div class="val path" id="vInstallDir"></div></div>
286312
<div class="row" id="vJobDockerBackendRow" style="display:none"><label>Docker 后端</label><div class="val"><code id="vJobDockerBackend"></code></div></div>
287313
<div class="row"><label>状态</label><div class="val"><span id="vStatus"></span><span id="vRunning"></span></div></div>
314+
<div class="row"><label>探测错误类型</label><div class="val" id="vProbeErrorType"></div></div>
315+
<div class="row"><label>建议操作</label><div class="val" id="vProbeSuggestion"></div></div>
316+
<div class="row"><label>检查命令(只读)</label><div class="val"><button type="button" id="vCopyCheckCmdBtn" class="btn-copy-cmd" style="display:none">复制命令</button><span id="vProbeCheckCommand"></span></div></div>
317+
<div class="row"><label>修复命令(有副作用)</label><div class="val"><button type="button" id="vRevealFixCmdBtn" class="btn-reveal-fix" style="display:none">显示修复命令</button><button type="button" id="vCopyFixCmdBtn" class="btn-copy-cmd" style="display:none">复制命令</button><span id="vProbeFixCommand"></span></div></div>
288318
<div class="row"><label>探测错误</label><div class="val" id="vProbeError"></div></div>
289319
<div class="row"><label>注册结果</label><div class="val" id="vRegistrationMessage"></div></div>
290320
<div class="row"><label>注册检查时间</label><div class="val" id="vRegistrationCheckedAt"></div></div>
@@ -443,13 +473,69 @@ <h3 id="modalTitle">Runner 配置</h3>
443473
d.textContent = s;
444474
return d.innerHTML;
445475
}
476+
function resolveProbeSuggestion(data) {
477+
if (data && data.probe && data.probe.suggestion) return data.probe.suggestion;
478+
return '请先尝试“停止/启动”进行自愈;若仍失败,查看 manager 与 runner 容器日志。';
479+
}
480+
function resolveProbeError(data) {
481+
if (data && data.probe && data.probe.error) return data.probe.error;
482+
return '';
483+
}
484+
function resolveProbeCheckCommand(data) {
485+
if (data && data.probe && data.probe.check_command) return data.probe.check_command;
486+
return 'docker compose ps && docker logs --tail=200 runner-manager';
487+
}
488+
function resolveProbeFixCommand(data) {
489+
if (data && data.probe && data.probe.fix_command) return data.probe.fix_command;
490+
return 'docker compose up -d --force-recreate';
491+
}
492+
function resolveProbeType(data) {
493+
if (data && data.probe && data.probe.type) return data.probe.type;
494+
return 'unknown';
495+
}
446496
const modal = document.getElementById('runnerModal');
447497
const modalTitle = document.getElementById('modalTitle');
448498
const modalView = document.getElementById('modalView');
449499
const modalEditForm = document.getElementById('modalEditForm');
450500
const modalEditBtn = document.getElementById('modalEditBtn');
451501
const modalSaveBtn = document.getElementById('modalSaveBtn');
452502
const modalMsg = document.getElementById('modalMsg');
503+
const revealFixBtn = document.getElementById('vRevealFixCmdBtn');
504+
const copyCheckBtn = document.getElementById('vCopyCheckCmdBtn');
505+
const copyFixBtn = document.getElementById('vCopyFixCmdBtn');
506+
const probeFixCmdEl = document.getElementById('vProbeFixCommand');
507+
const probeCheckCmdEl = document.getElementById('vProbeCheckCommand');
508+
let currentProbeCheckCommand = '';
509+
let currentProbeFixCommand = '';
510+
let probeFixRevealed = false;
511+
512+
async function copyCommandText(text) {
513+
if (!text) return false;
514+
try {
515+
if (navigator.clipboard && navigator.clipboard.writeText) {
516+
await navigator.clipboard.writeText(text);
517+
return true;
518+
}
519+
} catch (_) {
520+
// ignore and fallback below
521+
}
522+
// 回退:在不支持/拒绝 clipboard API 的环境里尝试 execCommand
523+
try {
524+
const ta = document.createElement('textarea');
525+
ta.value = text;
526+
ta.setAttribute('readonly', '');
527+
ta.style.position = 'fixed';
528+
ta.style.top = '-9999px';
529+
document.body.appendChild(ta);
530+
ta.select();
531+
ta.setSelectionRange(0, ta.value.length);
532+
const ok = document.execCommand && document.execCommand('copy');
533+
document.body.removeChild(ta);
534+
return !!ok;
535+
} catch (_) {
536+
return false;
537+
}
538+
}
453539

454540
function openModal(mode, name) {
455541
modalMsg.style.display = 'none';
@@ -479,7 +565,18 @@ <h3 id="modalTitle">Runner 配置</h3>
479565
}
480566
document.getElementById('vStatus').innerHTML = '<span class="badge ' + escapeHtml(data.status || '') + '">' + escapeHtml(data.status || '') + '</span>';
481567
document.getElementById('vRunning').innerHTML = data.running ? ' <span class="badge running">运行中</span>' : '';
482-
document.getElementById('vProbeError').textContent = data.probe_error || '—';
568+
const probeError = resolveProbeError(data);
569+
document.getElementById('vProbeErrorType').textContent = probeError ? resolveProbeType(data) : '—';
570+
document.getElementById('vProbeSuggestion').textContent = probeError ? resolveProbeSuggestion(data) : '—';
571+
currentProbeCheckCommand = probeError ? resolveProbeCheckCommand(data) : '';
572+
probeCheckCmdEl.textContent = currentProbeCheckCommand || '—';
573+
copyCheckBtn.style.display = currentProbeCheckCommand ? 'inline-block' : 'none';
574+
currentProbeFixCommand = probeError ? resolveProbeFixCommand(data) : '';
575+
probeFixRevealed = false;
576+
probeFixCmdEl.textContent = probeError ? '(默认隐藏,点击“显示修复命令”)' : '—';
577+
revealFixBtn.style.display = probeError ? 'inline-block' : 'none';
578+
copyFixBtn.style.display = 'none';
579+
document.getElementById('vProbeError').textContent = probeError || '—';
483580
document.getElementById('vRegistrationMessage').textContent = data.registration_message || '—';
484581
document.getElementById('vRegistrationCheckedAt').textContent = data.registration_checked_at || '—';
485582
var gh = data.registered_on_github;
@@ -497,6 +594,11 @@ <h3 id="modalTitle">Runner 配置</h3>
497594
startStopSpan.style.display = 'inline-block';
498595
startBtn.style.display = data.running ? 'none' : 'inline-block';
499596
stopBtn.style.display = data.running ? 'inline-block' : 'none';
597+
} else if (data.status === 'unknown') {
598+
// 状态探测失败时允许用户手动尝试启停进行自愈
599+
startStopSpan.style.display = 'inline-block';
600+
startBtn.style.display = 'inline-block';
601+
stopBtn.style.display = 'inline-block';
500602
} else {
501603
startBtn.style.display = 'none';
502604
stopBtn.style.display = 'none';
@@ -600,14 +702,45 @@ <h3 id="modalTitle">Runner 配置</h3>
600702
const r = await fetch('/api/runners/' + encodeURIComponent(name) + '/' + action, { method: 'POST' });
601703
const data = await r.json().catch(() => ({}));
602704
if (r.ok) {
603-
if (data && data.probe_error) {
604-
alert((data.message || '操作已执行') + '\n\n探测错误:' + data.probe_error);
705+
const probeError = resolveProbeError(data);
706+
if (probeError) {
707+
const probeType = resolveProbeType(data);
708+
const checkMsg =
709+
(data.message || '操作已执行') +
710+
'\n\n探测错误类型:' + probeType +
711+
'\n建议:' + resolveProbeSuggestion(data) +
712+
'\n检查命令:' + resolveProbeCheckCommand(data) +
713+
'\n探测错误:' + probeError;
714+
alert(checkMsg);
715+
if (confirm('是否显示修复命令(有副作用)?建议先执行检查命令确认。')) {
716+
alert('修复命令:\n' + resolveProbeFixCommand(data));
717+
}
605718
}
606719
location.reload();
607720
}
608721
else { alert(data.message || r.statusText || '请求失败'); }
609722
} catch (e) { alert(e.message); }
610723
}
724+
revealFixBtn.addEventListener('click', () => {
725+
if (!currentProbeFixCommand) return;
726+
if (!confirm('修复命令可能有副作用,确认显示?')) return;
727+
probeFixCmdEl.textContent = currentProbeFixCommand;
728+
probeFixRevealed = true;
729+
copyFixBtn.style.display = 'inline-block';
730+
});
731+
copyCheckBtn.addEventListener('click', async () => {
732+
if (!currentProbeCheckCommand) return;
733+
const ok = await copyCommandText(currentProbeCheckCommand);
734+
alert(ok ? '检查命令已复制' : '复制失败,请手动复制');
735+
});
736+
copyFixBtn.addEventListener('click', async () => {
737+
if (!probeFixRevealed || !currentProbeFixCommand) {
738+
alert('请先显示修复命令并确认后再复制');
739+
return;
740+
}
741+
const ok = await copyCommandText(currentProbeFixCommand);
742+
alert(ok ? '修复命令已复制' : '复制失败,请手动复制');
743+
});
611744
document.querySelectorAll('.btn-start').forEach(btn => {
612745
if (btn.id === 'modalStartBtnFooter') return;
613746
btn.addEventListener('click', () => runnerAction(btn.getAttribute('data-name'), 'start'));

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
本目录为 Runner Fleet 的详细文档,主文档见 [README](../README.md)
44

5+
> 接口变更提示:探测失败信息现仅通过结构化 `probe` 返回,历史扁平字段 `probe_*` 已移除。详见 [开发与构建](development.md) 的 HTTP API 说明与示例。
6+
57
| 文档 | 说明 |
68
|------|------|
79
| [配置说明](config.md) | `config.yaml` 各字段说明与示例 |

docs/adding-runner.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@
4949
- **注册结果**:仅使用添加时在表单中填写的 Token(从 GitHub 复制的短期 token)。每次在界面使用 Token 执行注册后,结果会写入该 runner 目录下的 `.registration_result.json`(成功或失败原因),并在列表与详情中显示。
5050
- **GitHub 显示检查**(可选):服务内建定时任务约每 5 分钟检查各 runner 是否已在 GitHub 的 Actions Runners 列表中显示。若需此功能,请在该 runner 安装目录下放置 `.github_check_token` 文件,内容为 PAT(需 scope:组织用 `admin:org`,仓库用 `repo`)。检查结果写入各 runner 目录的 `.github_status.json`,并在界面「注册 / GitHub」列与详情中展示。
5151

52+
## 探测失败与自愈(容器模式)
53+
54+
当列表状态显示 `unknown` 时,表示 Manager 无法完成容器状态探测(例如 Docker 访问异常、Agent 不可达、Agent HTTP 错误),并不代表「启动/停止」一定失败。
55+
56+
- 详情弹窗会展示结构化 `probe` 信息(错误类型、建议、检查命令、修复命令)。
57+
- 建议先执行「检查命令(只读)」定位问题,再决定是否执行「修复命令(有副作用)」。
58+
-`status=unknown` 时界面仍保留「启动 / 停止」按钮,可用于手动自愈尝试。
59+
5260
## 一台机器多 Runner
5361

5462
每个 Runner 使用独立子目录(如 `runners/runner-1``runners/runner-2`),互不干扰,可同时运行多个 Runner 并行执行任务。

docs/development.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,40 @@ make run
5454
|------|------|------|
5555
| `/health` | GET | 返回 `{"status":"ok"}`,可用于 Ingress/K8s 探针。 |
5656
| `/version` | GET | 返回 `{"version":"..."}`|
57-
| `/api/runners` | GET | 返回 Runner 列表。容器模式下若状态探测失败,会返回 `status=unknown` 且带 `probe_error`|
58-
| `/api/runners/:name` | GET | 返回单个 Runner 详情。容器模式下若状态探测失败,同样返回 `probe_error` 便于排障。 |
59-
| `/api/runners/:name/start` | POST | 启动指定 Runner。容器模式下若状态探测失败,仍会尝试启动,并在响应中返回 `probe_error`|
60-
| `/api/runners/:name/stop` | POST | 停止指定 Runner。容器模式下若状态探测失败,仍会尝试停止,并在响应中返回 `probe_error`|
57+
| `/api/runners` | GET | 返回 Runner 列表。容器模式下若状态探测失败,会返回 `status=unknown` 且带结构化 `probe`(含 `error/type/suggestion/check_command/fix_command`)。 |
58+
| `/api/runners/:name` | GET | 返回单个 Runner 详情。容器模式下若状态探测失败,同样返回结构化 `probe`|
59+
| `/api/runners/:name/start` | POST | 启动指定 Runner。容器模式下若状态探测失败,仍会尝试启动,并在响应中返回结构化 `probe`|
60+
| `/api/runners/:name/stop` | POST | 停止指定 Runner。容器模式下若状态探测失败,仍会尝试停止,并在响应中返回结构化 `probe`|
61+
62+
### 升级注意(破坏性变更)
63+
64+
- 探测失败相关的历史扁平字段(`probe_error``probe_error_type``probe_suggestion``probe_check_command``probe_fix_command`)已移除。
65+
- 调用方需统一读取 `probe` 对象:
66+
- 错误文本:`probe.error`
67+
- 错误类型:`probe.type`
68+
- 建议与命令:`probe.suggestion``probe.check_command``probe.fix_command`
69+
- 若你有自定义前端、告警或脚本,请将解析逻辑从 `probe_*` 切换到 `probe.*`
70+
71+
WebUI 在 `status=unknown` 时会保留「启动 / 停止」手动操作入口,便于运维在探测异常时执行自愈动作。
72+
`probe.type` 目前可能值:`docker-access``agent-http``agent-connect``unknown`
73+
WebUI 优先展示后端返回的 `probe` 字段(后端为建议与命令单点来源),前端仅保留兜底通用提示。
74+
WebUI 会将命令拆分为「检查命令(只读)」与「修复命令(有副作用)」,默认建议先执行检查命令;修复命令需要二次确认后才显示。两类命令都支持一键复制(仅复制,不执行),并带浏览器权限受限时的回退复制逻辑。
75+
76+
示例(探测失败):
77+
78+
```json
79+
{
80+
"name": "runner-a",
81+
"status": "unknown",
82+
"probe": {
83+
"error": "agent 返回 502: bad gateway",
84+
"type": "agent-http",
85+
"suggestion": "查看 runner 容器日志,确认 Agent 与 /runner 下脚本进程状态",
86+
"check_command": "docker ps -a | rg \"github-runner-\" && docker logs --tail=200 <runner_container_name>",
87+
"fix_command": "docker restart <runner_container_name>"
88+
}
89+
}
90+
```
6191

6292
## Makefile 目标
6393

docs/docker.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,21 @@ docker run -d --name runner-manager \
212212
- **回滚**:将 `job_docker_backend` 设为 `dind` 并确保 DinD 已启动(`docker compose --profile dind up -d`)。
213213
- **可选**:改为 `host-socket` 时需在创建 Runner 容器时挂载宿主机 socket,Compose 已为 Manager 挂载;Runner 容器由 Manager 按 `host-socket` 自动注入挂载与 `DOCKER_HOST`。改为 `none` 时 Job 内无法使用 Docker。
214214

215+
### 状态 `unknown` 的处理(容器模式)
216+
217+
当列表中 Runner 显示 `status=unknown` 时,表示 Manager 对该 Runner 的状态探测失败。常见原因包括:
218+
219+
- Manager 无法访问 Docker(权限、socket、daemon)。
220+
- Manager 无法连通 Runner 容器内 Agent(网络或容器未就绪)。
221+
- Agent 返回非 200(Runner 进程异常、容器内依赖异常)。
222+
223+
排查建议:
224+
225+
1. 在 WebUI 详情弹窗查看 `probe` 字段(`error/type/suggestion/check_command/fix_command`)。
226+
2. 先执行 `probe.check_command`(只读)确认问题边界。
227+
3. 再按需执行 `probe.fix_command`(有副作用)。
228+
4. 在 `status=unknown` 时可直接尝试 UI 的「启动 / 停止」按钮进行自愈;接口会继续尝试执行启停动作。
229+
215230
### 最小验收清单
216231

217232
- [ ] WebUI 可新增一个项目下多个 Runner,并能独立启动/停止。

0 commit comments

Comments
 (0)