fix(installer): 进程探针区分「查不了」与「没在跑」,并给 e2e 补 status --json 的路径断言
## 本笔包含什么
1. **进程探针三态化**(`_agentPids` → `probeAgentProcess()` / `AgentProcessProbe`)
- 原实现 `Process.run('/bin/zsh', ['-c', 'pgrep -f ...'])`:**Linux 上 `/bin/zsh`
通常不存在** → `ProcessException` 直接抛穿 `install` / `uninstall` / `status`;
Windows 上 `pgrep` 一定没有 → 返回空列表,于是"查不了"被显示成"未运行"
(用户会去重装,而真问题是本平台没有探针)。
- 改为**不经 shell**直调 `pgrep -f <RegExp.escape 后的绝对路径> --collector`:
macOS/Linux 通用,且路径含空格/引号时不再交给 shell 解析。
- 返回值带 `available`:`pgrep` 退出码 1 = 确实没匹配(唯一可信的"没在跑"),
其它非零 / `ProcessException` = **无法判定**。上层:`install` 不把"查不了"
算成失败(只 ⚠),`uninstall` 不把"查不了"说成"已停止"(只 ⚠),
`status --json` 增 `process.probe_available/probe_tool/probe_error`,
人话输出显示「采集端: 无法判定(原因)」。只增字段,`running`/`pids` 语义不变,
GUI 侧解析不受影响。
- **Windows 未实现**(刻意):写一条未经真机验证的 PowerShell `-like` 分支,
只会"看起来能判定",匹配一错就是又一次"用错事实下判断"。已留
`TODO(多端前置)` 并明确报"无法判定"。
2. **`tests/installer-clean-e2e.js` 新增 8 项 `status --json` 断言**(26 → 34 项)
- 解析策略按 team-lead 要求与 GUI 侧一致:**从后往前**扫行、行内从后往前找 `{`
试解析 —— 因为 `dart run` 会把 `Running build hooks...` **拼在 JSON 同一行前面**
(实测 `Running build hooks...Running build hooks...{"label":...}`),
按"整行是 JSON"判断会把唯一有效输出判成解析失败。
- 断言 `config.path` / `installed.agent_path` / `autostart.unit_path` 与真相源
按**临时 HOME** 推导的值逐字相等 —— 硬编码旧值不可能跟着临时 HOME 变,
故这是"路径收敛真的生效"的最强证据。
3. `statusSnapshot` 里 `log.path` 加 `TODO`:这是**唯一还没收敛**的路径
(Rust 的 `macos_launchagent_plist` 把 `StandardErrorPath` 写成同一形状,
Dart 侧又写一份)。正确做法与 `allow_failure` 同款(让 `AutoStartPlan` 多返回
`stderr_log_path`),但需改 Rust 数据结构 + FFI 出口,超出"client 路径"边界,本轮未做。
## 归属说明(team-lead 要求留痕)
- `pmon_install.dart` 的 `statusSnapshot()` + `--json` 改动由**另一位工程师**实现;
因与路径收敛同处一个文件、非交互环境下无法按 hunk 拆分,实际已在上一笔
**c152ea2** 一并落地(该文件在 c152ea2 里是 151+/37-,即两人改动的合并体)。
本笔只在其上追加"探针三态化 + e2e 断言 + TODO",未改动其判据逻辑。
- 反向留痕:`51c85b0`(team-lead 记作 `3ee0630`,该 hash 在全部历史与分支中都不存在,
实际是 `51c85b0 test(常驻采集): 把「CLI 输出 ↔ GUI 解析器」两端的契约钉在一起`)
是"测试先行"那一笔 —— 它 import `installer.statusSnapshot`,而当时 CLI 里
`statusSnapshot` 出现次数为 **0**,单独 checkout 会因编译失败而红;
**本笔的前置 c152ea2 使其转绿**。保留它不 revert,两侧都留痕,避免 `git bisect`
把"顺序"读成"回归"。
## 验证(改前 → 改后)
- `cargo test --release`:61/0 → **61/0**
- `flutter test`:540/0 → **545/0**(含 `installer_status_json_test.dart` 4 项)
- `dart analyze lib bin`:**No issues found!**
- `bash tests/run-all.sh`:31 套件 30/1 → **31 套件 30/1**(唯一红仍是隐私扫描,未碰)
- `node tests/installer-clean-e2e.js`:26/0 → **34 通过 / 0 失败 / 1 跳过**
(跳过=launchctl `Bootstrap failed: 5` 环境受限,原因逐条打印)
- `guard-structure-test.js`:42/0
## 已核实的现场事实(供 team-lead 复核)
`status --json` 在**真实 HOME** 与**临时 HOME** 下都取到正确路径,后者全部跟着临时
HOME 走,证明三个路径字段来自 `app_paths.dart` 推导而非硬编码:
- 真实:`config.path=/Users/me/Library/Application Support/pmon_client/config.json`、
`installed.agent_path=.../bin/pmon-agent`、`log.path=/tmp/com.pmon.collector.err.log`
- 临时:`config.path=/tmp/pmon-json-probe-xxxx/Library/Application Support/pmon_client/config.json`、`installed.agent_path` 同源变化、
且 `process.running=false`(**正版 pid 1737 不再被算进来**,路径作用域生效)
This commit is contained in:
1 parent
cb82678cdd
commit
24e79b06a6
3 files changed
+190
-32
No files matched your search
+130
-29
@@ -24,12 +24,16 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:pmon_client/host/app_config.dart';
|
||||
import 'package:pmon_client/host/autostart_label.dart';
|
||||
import 'package:pmon_client/host/autostart_runner.dart';
|
||||
import 'package:pmon_client/host/install_paths.dart';
|
||||
import 'package:pmon_client/host/pmon_core.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
const _defaultLabel = 'com.pmon.collector';
|
||||
// label 的唯一真相源是 `host/autostart_label.dart` —— GUI 的 `ResidentService`
|
||||
// 查状态时传的是**同一个常量**。曾经两处各写一份字面量,漂移后的症状是
|
||||
// "明明装了、卡片却显示未注册"(问的不是同一个自启项)。
|
||||
const _defaultLabel = kDefaultAutostartLabel;
|
||||
|
||||
Future<void> main(List<String> argv) async {
|
||||
// ⚠️ 必须显式写 `exitCode`:本 Dart SDK **不再把 `int main()` 的返回值当退出码**
|
||||
@@ -158,12 +162,18 @@ Future<int> _install(Map<String, Object> opts, String label) async {
|
||||
var hardFailures = outcome.hardFailures;
|
||||
|
||||
if (opts['no-start'] != true) {
|
||||
final pids = await _waitForAgent(const Duration(seconds: 3));
|
||||
if (pids.isEmpty) {
|
||||
final probe = await _waitForAgent(const Duration(seconds: 3));
|
||||
if (!probe.available) {
|
||||
// 「查不了」不能算成「没起来」:非 macOS/Linux 上可能没有 pgrep,
|
||||
// 那时把安装判成失败等于告诉用户"装了但没起",而实际什么都没查过。
|
||||
// 只如实警告,不把不确定变成失败(也不变成成功)。
|
||||
stdout.writeln(' ⚠ 本平台无法判定采集端进程(${probe.error}),'
|
||||
'请用 status 或系统工具确认');
|
||||
} else if (!probe.running) {
|
||||
hardFailures++;
|
||||
stderr.writeln(' ✗ 未检测到采集端进程(已等 3 秒)');
|
||||
} else {
|
||||
stdout.writeln(' ✓ 采集端已运行(pid ${pids.join(",")})');
|
||||
stdout.writeln(' ✓ 采集端已运行(pid ${probe.pids.join(",")})');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,14 +206,19 @@ Future<int> _uninstall(String label) async {
|
||||
_printAutostartOutcome(outcome);
|
||||
final hardFailures = outcome.hardFailures;
|
||||
final stopped = await _confirmStopped();
|
||||
if (hardFailures > 0 || !stopped) {
|
||||
if (hardFailures > 0 || stopped == false) {
|
||||
stderr.writeln('\n卸载未完成。');
|
||||
if (hardFailures > 0) stderr.writeln(' · $hardFailures 项必成命令失败');
|
||||
if (!stopped) stderr.writeln(' · 采集端进程仍在运行(已等 5 秒)');
|
||||
if (stopped == false) stderr.writeln(' · 采集端进程仍在运行(已等 5 秒)');
|
||||
stderr.writeln('排查:pmon-install status');
|
||||
return 1;
|
||||
}
|
||||
stdout.writeln(' ✓ 已停止并移除自启');
|
||||
if (stopped == null) {
|
||||
// 探针不可用:不能据此宣布"已停止",也不能判失败(那会把环境限制说成缺陷)
|
||||
stdout.writeln(' ⚠ 已移除自启项,但本平台无法判定采集端进程是否已退出');
|
||||
} else {
|
||||
stdout.writeln(' ✓ 已停止并移除自启项');
|
||||
}
|
||||
stdout.writeln('(采集端文件与配置保留在 $installRoot,如不需要可自行删除)');
|
||||
return 0;
|
||||
}
|
||||
@@ -239,8 +254,15 @@ Future<Map<String, Object?>> statusSnapshot(String label) async {
|
||||
}
|
||||
}
|
||||
|
||||
final pids = await _agentPids();
|
||||
final probe = await probeAgentProcess();
|
||||
final cfg = await ConfigStore.load();
|
||||
// TODO(单一真相源 / 第 15 条收尾):这个 `/tmp/<label>.err.log` 是**唯一**还没收敛的
|
||||
// 路径——Rust 的 `macos_launchagent_plist` 把 `StandardErrorPath` 写成同一形状,
|
||||
// 也就是说这条事实在 Rust 与 Dart 各写一份(改一处就"status 找错日志文件")。
|
||||
// 正确做法与 `allow_failure` 同款:让 `AutoStartPlan` 额外返回 `stderr_log_path`,
|
||||
// Dart 只读不算。**不在本轮做**:要改 Rust 侧数据结构 + FFI 出口,超出"client 路径"边界。
|
||||
// 另注:非 macOS 上 `/tmp` 未必存在(Windows 一定没有),但那里也没有 launchd
|
||||
// `StandardErrorPath` 这回事,故该字段在采集端日志上本就无意义,如实显示"(无)"。
|
||||
final logPath = '/tmp/$label.err.log';
|
||||
|
||||
return {
|
||||
@@ -254,8 +276,13 @@ Future<Map<String, Object?>> statusSnapshot(String label) async {
|
||||
'detail_lines': detail,
|
||||
},
|
||||
'process': {
|
||||
'running': pids.isNotEmpty,
|
||||
'pids': pids,
|
||||
'running': probe.running,
|
||||
'pids': probe.pids,
|
||||
// 「查不了」≠「没在跑」:GUI 侧据此显示"无法判定"而不是"未运行"。
|
||||
// 只增字段,不改 `running`/`pids` 的既有语义(GUI 已按这两个字段解析)。
|
||||
'probe_available': probe.available,
|
||||
'probe_tool': probe.tool,
|
||||
'probe_error': probe.error,
|
||||
},
|
||||
'config': {
|
||||
'path': ConfigStore.path,
|
||||
@@ -314,7 +341,12 @@ Future<int> _status(String label, {bool json = false}) async {
|
||||
|
||||
stdout.writeln('=== 进程 ===');
|
||||
final pids = ((process['pids'] as List?) ?? const []).map((e) => '$e').toList();
|
||||
stdout.writeln(' 采集端: ${pids.isEmpty ? "未运行" : "pid ${pids.join(",")}"}');
|
||||
if (process['probe_available'] == false) {
|
||||
// 如实说"无法判定",不要拿"未运行"冒充结论(那会让人去重装)
|
||||
stdout.writeln(' 采集端: 无法判定(${process['probe_error']})');
|
||||
} else {
|
||||
stdout.writeln(' 采集端: ${pids.isEmpty ? "未运行" : "pid ${pids.join(",")}"}');
|
||||
}
|
||||
|
||||
stdout.writeln('=== 配置 ===');
|
||||
stdout.writeln(' ${config['path']}');
|
||||
@@ -351,12 +383,16 @@ Future<int> _restart(String label) async {
|
||||
return 1;
|
||||
}
|
||||
stdout.writeln(' ✓ kickstart 已提交');
|
||||
final pids = await _waitForAgent(const Duration(seconds: 3));
|
||||
if (pids.isEmpty) {
|
||||
final probe = await _waitForAgent(const Duration(seconds: 3));
|
||||
if (!probe.available) {
|
||||
stdout.writeln(' ⚠ 本平台无法判定采集端进程(${probe.error})');
|
||||
return 0;
|
||||
}
|
||||
if (!probe.running) {
|
||||
stderr.writeln(' ✗ 3 秒内未见到采集端进程');
|
||||
return 1;
|
||||
}
|
||||
stdout.writeln(' 采集端: pid ${pids.join(",")}');
|
||||
stdout.writeln(' 采集端: pid ${probe.pids.join(",")}');
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -505,16 +541,48 @@ void _printAutostartOutcome(AutostartRunOutcome outcome) {
|
||||
///
|
||||
/// 用 `Stopwatch` 而非 `DateTime.now()`:这里虽是短任务,但"时钟语义"
|
||||
/// 在本仓已经因墙钟踩过坑(NTP 回拨让重连挂死),不要再引入第二处口径。
|
||||
Future<List<String>> _waitForAgent(Duration timeout) async {
|
||||
Future<AgentProcessProbe> _waitForAgent(Duration timeout) async {
|
||||
final sw = Stopwatch()..start();
|
||||
while (true) {
|
||||
final pids = await _agentPids();
|
||||
if (pids.isNotEmpty || sw.elapsed >= timeout) return pids;
|
||||
final probe = await probeAgentProcess();
|
||||
// 探针不可用就立刻返回:再等下去也不会出现"可判定"的状态,
|
||||
// 只会把"查不了"拖成一次假超时(然后被当成"没起来")。
|
||||
if (!probe.available || probe.running || sw.elapsed >= timeout) return probe;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 400));
|
||||
}
|
||||
}
|
||||
|
||||
/// 采集端进程 pid(**只认本机这套安装**下的采集端)
|
||||
/// 进程探测结果 —— **「查不了」与「没在跑」必须分开**。
|
||||
///
|
||||
/// 为什么不能只返回 `List<String>`:Linux 精简体 / Windows 上可能根本没有
|
||||
/// `pgrep`,那时返回空列表就等于宣称"采集端没在跑" —— 用户会去重装,
|
||||
/// 而真问题是"本平台没有探针"。本项目对"静默把不确定说成确定"零容忍,
|
||||
/// 故把可用性显式带出来,让上层如实显示"无法判定"。
|
||||
class AgentProcessProbe {
|
||||
const AgentProcessProbe._({
|
||||
required this.available,
|
||||
required this.pids,
|
||||
required this.tool,
|
||||
required this.error,
|
||||
});
|
||||
|
||||
/// 探到了([pids] 为空表示确实没在跑)
|
||||
factory AgentProcessProbe.found(List<String> pids, String tool) =>
|
||||
AgentProcessProbe._(available: true, pids: pids, tool: tool, error: '');
|
||||
|
||||
/// 本平台无法判定(不是"没在跑")
|
||||
factory AgentProcessProbe.unavailable(String tool, String error) =>
|
||||
AgentProcessProbe._(available: false, pids: const [], tool: tool, error: error);
|
||||
|
||||
final bool available;
|
||||
final List<String> pids;
|
||||
final String tool;
|
||||
final String error;
|
||||
|
||||
bool get running => pids.isNotEmpty;
|
||||
}
|
||||
|
||||
/// 采集端进程探针(**只认本机这套安装**下的采集端)
|
||||
///
|
||||
/// ⚠️ 必须按「落位后的绝对路径 + --collector」匹配,不能只匹配 `pmon-agent --collector`:
|
||||
/// 开发机上常另有一份采集端在跑(例如用户自己装过的正式版),只按名字匹配会把
|
||||
@@ -523,25 +591,58 @@ Future<List<String>> _waitForAgent(Duration timeout) async {
|
||||
/// · `uninstall` 明明已摘干净却因为"进程仍在运行"返回非零(假失败)。
|
||||
/// 两种都是"用错事实下判断",比报错更难查。
|
||||
/// 路径取自 `install_paths.dart`(唯一真相源),这里不再拼任何路径。
|
||||
Future<List<String>> _agentPids() async {
|
||||
Future<AgentProcessProbe> probeAgentProcess() async {
|
||||
if (Platform.isWindows) {
|
||||
// TODO(多端前置 / 见 team-lead 多端清单):Windows 没有 pgrep。
|
||||
// 可行做法是 `powershell -NoProfile -Command "Get-CimInstance Win32_Process | ..."`
|
||||
// (`tasklist` 只给进程名、拿不到命令行,匹配不到 `--collector`)。
|
||||
// **本轮刻意不写这条未经真机验证的分支**:写了只会"看起来能判定",
|
||||
// `-like` 匹配一旦写错就是又一次"用错事实下判断"。故明确报"无法判定"。
|
||||
return AgentProcessProbe.unavailable('', 'Windows 尚无进程探针(待多端扩面实现)');
|
||||
}
|
||||
// 用 RegExp.escape 把路径变成字面量模式:pgrep -f 收的是 ERE,
|
||||
// 家目录里出现 `.` 之类字符时不该被当通配符(否则仍会匹配到别人)。
|
||||
final pattern = RegExp.escape('$installedAgentPath --collector')
|
||||
.replaceAll("'", r"'\''");
|
||||
final r = await Process.run('/bin/zsh', ['-c', "pgrep -f '$pattern' || true"]);
|
||||
return (r.stdout as String)
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
final pattern = RegExp.escape('$installedAgentPath --collector');
|
||||
try {
|
||||
// **不经 shell**:Linux 上 `/bin/zsh` 通常不存在(合并前的
|
||||
// `/bin/zsh -c 'pgrep ...'` 在 Linux 上会直接抛 ProcessException,
|
||||
// 于是 install / uninstall / status 全部崩掉),且路径含空格或引号时
|
||||
// 也不该交给 shell 去解析。
|
||||
final r = await Process.run('pgrep', ['-f', pattern]);
|
||||
if (r.exitCode == 1) {
|
||||
// pgrep 的 1 = 没有匹配(这是"确实没在跑"的**唯一**可信来源)
|
||||
return AgentProcessProbe.found(const [], 'pgrep');
|
||||
}
|
||||
if (r.exitCode != 0) {
|
||||
return AgentProcessProbe.unavailable(
|
||||
'pgrep', 'exit ${r.exitCode}: ${(r.stderr as String).trim()}');
|
||||
}
|
||||
final pids = (r.stdout as String)
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
return AgentProcessProbe.found(pids, 'pgrep');
|
||||
} on ProcessException catch (e) {
|
||||
return AgentProcessProbe.unavailable('pgrep', '本平台没有可用的 pgrep:${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 默认设备标识:主机名(与服务端一眼能对上是哪台机器)
|
||||
String _defaultDeviceId() => Platform.localHostname;
|
||||
|
||||
Future<bool> _confirmStopped() async {
|
||||
/// 卸载后确认采集端确实退出。
|
||||
///
|
||||
/// 返回语义刻意是 `bool?`:
|
||||
/// `true` 已确认退出
|
||||
/// `false` 5 秒内仍能看到进程 → 卸载未完成
|
||||
/// `null` **本平台无法判定**(探针不可用)—— 既不能宣布成功,也不能判失败;
|
||||
/// 这一支由调用方打印明确的 ⚠,而不是拿不确定当结论。
|
||||
Future<bool?> _confirmStopped() async {
|
||||
for (var i = 0; i < 10; i++) {
|
||||
if ((await _agentPids()).isEmpty) return true;
|
||||
final probe = await probeAgentProcess();
|
||||
if (!probe.available) return null;
|
||||
if (!probe.running) return true;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -187,6 +187,31 @@ function looksEnvironmentLimited(text) {
|
||||
&& /launchd|launchctl|Input\/output error|systemctl|bootout/i.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 CLI 输出里取出 JSON 对象。
|
||||
*
|
||||
* ⚠️ **不能按"整行是 JSON"判断**:`dart run` 会把 `Running build hooks...`
|
||||
* 拼在 JSON 的**同一行前面**(实测:`Running build hooks...Running build hooks...{"label":...}`)。
|
||||
* 按行首 `{` 判断会把唯一有效的输出判成解析失败。
|
||||
* 策略与 GUI 侧解析器(`ResidentService`)保持一致,**不另写一套**:
|
||||
* 从后往前扫行、每行从后往前找 `{`,取第一个能解析成对象的。
|
||||
*/
|
||||
function pickJsonObject(text) {
|
||||
const lines = String(text).split('\n');
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i];
|
||||
for (let j = line.lastIndexOf('{'); j >= 0; j = line.lastIndexOf('{', j - 1)) {
|
||||
try {
|
||||
const v = JSON.parse(line.slice(j));
|
||||
if (v && typeof v === 'object' && !Array.isArray(v)) return v;
|
||||
} catch (_) {
|
||||
// 这个 `{` 不是 JSON 起点,继续往前找
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('=== 首次安装路径 e2e(干净 HOME)===');
|
||||
console.log(` HOME = ${HOME}`);
|
||||
console.log(` dart = ${DART}`);
|
||||
@@ -308,6 +333,38 @@ ok('status 报进程段(未运行也要如实说)',
|
||||
ok('status 报日志段(无日志也要如实说)',
|
||||
/=== 最近日志/.test(rStatus.all), rStatus.all.slice(0, 400));
|
||||
|
||||
/* ⑥b ★ `status --json`:GUI 读的那一端,路径字段必须与真相源一致
|
||||
*
|
||||
* 这是"路径收敛有没有真的生效"的最强证据:JSON 里的路径若是硬编码的旧值,
|
||||
* 换成临时 HOME 也不会变;反之只要它们跟着 HOME 走,就证明是 app_paths 推导的。
|
||||
*/
|
||||
const rJson = installer(['status', '--json', '--label', LABEL]);
|
||||
ok('status --json 退出码为 0', rJson.status === 0, `exit=${rJson.status}`);
|
||||
const snap = pickJsonObject(rJson.all);
|
||||
ok('status --json 可解析(前缀噪声 "Running build hooks..." 不影响解析)',
|
||||
!!snap, rJson.all.slice(0, 300));
|
||||
if (snap) {
|
||||
ok('JSON config.path == 真相源按临时 HOME 推导的路径',
|
||||
snap.config && snap.config.path === configPath,
|
||||
`json=${snap.config && snap.config.path} 期望=${configPath}`);
|
||||
ok('JSON installed.agent_path == 落位后的可执行文件',
|
||||
snap.installed && snap.installed.agent_path === path.join(binDir, agentName),
|
||||
`json=${snap.installed && snap.installed.agent_path}`);
|
||||
ok('JSON autostart.unit_path 与 plist 落位一致(非 macOS 应为 null)',
|
||||
snap.autostart && snap.autostart.unit_path === plistPath,
|
||||
`json=${snap.autostart && snap.autostart.unit_path} 期望=${plistPath}`);
|
||||
ok('JSON process.pids 是字符串数组(GUI 按字符串解析)',
|
||||
snap.process && Array.isArray(snap.process.pids)
|
||||
&& snap.process.pids.every((x) => typeof x === 'string'),
|
||||
JSON.stringify(snap.process));
|
||||
ok('JSON process.probe_available 是布尔("查不了"要能与"没在跑"区分)',
|
||||
snap.process && typeof snap.process.probe_available === 'boolean',
|
||||
JSON.stringify(snap.process));
|
||||
ok('JSON 顶层四类判据齐全(autostart/process/config/log)',
|
||||
['autostart', 'process', 'config', 'log'].every((k) => snap[k] && typeof snap[k] === 'object'),
|
||||
Object.keys(snap).join(','));
|
||||
}
|
||||
|
||||
/* ⑦ 卸载:摘干净自启,保留文件(如实说明) */
|
||||
const rUninstall = installer(['uninstall', '--label', LABEL]);
|
||||
ok('uninstall 退出码为 0', rUninstall.status === 0,
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@
|
||||
# [E] 段守「全新设备首次接入」(认领即注册,防死锁回归);
|
||||
# [F] 段守「指标/心跳通道不得绕过设备令牌」(防只守 hello 的漏洞回归)。
|
||||
# 查看端端到端 21 项需业务服务 + dart + 已构建的 Rust 核心。
|
||||
# 安装器干净目录 26 项(E10a)需 dart + Rust 核心产物 + 采集端可执行文件;
|
||||
# 安装器干净目录 34 项(E10a)需 dart + Rust 核心产物 + 采集端可执行文件;
|
||||
# 全程在临时 HOME 内跑,不碰真实家目录;缺依赖时明确跳过并返回成功。
|
||||
# 中继路由端到端 14 项需网关;中继客户端端到端 6 项还需 dart(无需 Rust 产物)。
|
||||
# 另外三个**纯离线**套件(不起服务,任何机器可跑):
|
||||
@@ -122,10 +122,10 @@ run "FFI 冒烟(79 项)" ${PMON_PYTHON:-python3} ffi-smoke.py
|
||||
# 缺 dart / 缺 Rust 核心产物 / 缺采集端可执行文件时,脚本自己打印 ⚠ 并返回成功。
|
||||
DART_BIN="${PMON_DART:-$HOME/development/flutter/bin/dart}"
|
||||
if [ -x "$DART_BIN" ] && [ -f ../rust-core/target/release/libpmon_core.dylib ]; then
|
||||
run "安装器干净目录 e2e(26 项)" node installer-clean-e2e.js
|
||||
run "安装器干净目录 e2e(34 项)" node installer-clean-e2e.js
|
||||
else
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠ 缺 dart 或 Rust 核心产物:跳过安装器干净目录 e2e(26 项)。${NC}"
|
||||
echo -e "${YELLOW}⚠ 缺 dart 或 Rust 核心产物:跳过安装器干净目录 e2e(34 项)。${NC}"
|
||||
echo " 需 (cd rust-core && cargo build --release) 与 dart。"
|
||||
fi
|
||||
|
||||
|
||||
Reference in new issue
Block a user